@@ -8,7 +8,6 @@ import type { ViteDevServer } from '../../server'
88import type { ResolvedConfig } from '../../config'
99import { FS_PREFIX } from '../../constants'
1010import {
11- fsPathFromId ,
1211 fsPathFromUrl ,
1312 isFileReadable ,
1413 isImportRequest ,
@@ -27,11 +26,16 @@ import {
2726} from '../../../shared/utils'
2827
2928const knownJavascriptExtensionRE = / \. (?: [ t j ] s x ? | [ c m ] [ t j ] s ) $ /
29+ const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
3030
3131const sirvOptions = ( {
32+ config,
3233 getHeaders,
34+ disableFsServeCheck,
3335} : {
36+ config : ResolvedConfig
3437 getHeaders : ( ) => OutgoingHttpHeaders | undefined
38+ disableFsServeCheck ?: boolean
3539} ) : Options => {
3640 return {
3741 dev : true ,
@@ -53,6 +57,22 @@ const sirvOptions = ({
5357 }
5458 }
5559 } ,
60+ shouldServe : disableFsServeCheck
61+ ? undefined
62+ : ( filePath ) => {
63+ const servingAccessResult = checkLoadingAccess ( config , filePath )
64+ if ( servingAccessResult === 'denied' ) {
65+ const error : any = new Error ( 'denied access' )
66+ error . code = ERR_DENIED_FILE
67+ error . path = filePath
68+ throw error
69+ }
70+ if ( servingAccessResult === 'fallback' ) {
71+ return false
72+ }
73+ servingAccessResult satisfies 'allowed'
74+ return true
75+ } ,
5676 }
5777}
5878
@@ -64,7 +84,9 @@ export function servePublicMiddleware(
6484 const serve = sirv (
6585 dir ,
6686 sirvOptions ( {
87+ config : server . config ,
6788 getHeaders : ( ) => server . config . server . headers ,
89+ disableFsServeCheck : true ,
6890 } ) ,
6991 )
7092
@@ -105,6 +127,7 @@ export function serveStaticMiddleware(
105127 const serve = sirv (
106128 dir ,
107129 sirvOptions ( {
130+ config : server . config ,
108131 getHeaders : ( ) => server . config . server . headers ,
109132 } ) ,
110133 )
@@ -154,16 +177,20 @@ export function serveStaticMiddleware(
154177 if ( resolvedPathname . endsWith ( '/' ) && fileUrl [ fileUrl . length - 1 ] !== '/' ) {
155178 fileUrl = withTrailingSlash ( fileUrl )
156179 }
157- if ( ! ensureServingAccess ( fileUrl , server , res , next ) ) {
158- return
159- }
160-
161180 if ( redirectedPathname ) {
162181 url . pathname = encodeURI ( redirectedPathname )
163182 req . url = url . href . slice ( url . origin . length )
164183 }
165184
166- serve ( req , res , next )
185+ try {
186+ serve ( req , res , next )
187+ } catch ( e ) {
188+ if ( e && 'code' in e && e . code === ERR_DENIED_FILE ) {
189+ respondWithAccessDenied ( e . path , server , res )
190+ return
191+ }
192+ throw e
193+ }
167194 }
168195}
169196
@@ -172,7 +199,10 @@ export function serveRawFsMiddleware(
172199) : Connect . NextHandleFunction {
173200 const serveFromRoot = sirv (
174201 '/' ,
175- sirvOptions ( { getHeaders : ( ) => server . config . server . headers } ) ,
202+ sirvOptions ( {
203+ config : server . config ,
204+ getHeaders : ( ) => server . config . server . headers ,
205+ } ) ,
176206 )
177207
178208 // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -184,24 +214,20 @@ export function serveRawFsMiddleware(
184214 if ( req . url ! . startsWith ( FS_PREFIX ) ) {
185215 const url = new URL ( req . url ! , 'http://example.com' )
186216 const pathname = decodeURI ( url . pathname )
187- // restrict files outside of `fs.allow`
188- if (
189- ! ensureServingAccess (
190- slash ( path . resolve ( fsPathFromId ( pathname ) ) ) ,
191- server ,
192- res ,
193- next ,
194- )
195- ) {
196- return
197- }
198-
199217 let newPathname = pathname . slice ( FS_PREFIX . length )
200218 if ( isWindows ) newPathname = newPathname . replace ( / ^ [ A - Z ] : / i, '' )
201-
202219 url . pathname = encodeURI ( newPathname )
203220 req . url = url . href . slice ( url . origin . length )
204- serveFromRoot ( req , res , next )
221+
222+ try {
223+ serveFromRoot ( req , res , next )
224+ } catch ( e ) {
225+ if ( e && 'code' in e && e . code === ERR_DENIED_FILE ) {
226+ respondWithAccessDenied ( e . path , server , res )
227+ return
228+ }
229+ throw e
230+ }
205231 } else {
206232 next ( )
207233 }
@@ -210,14 +236,12 @@ export function serveRawFsMiddleware(
210236
211237/**
212238 * Check if the url is allowed to be served, via the `server.fs` config.
239+ * @deprecated Use the `isFileLoadingAllowed` function instead.
213240 */
214241export function isFileServingAllowed (
215242 config : ResolvedConfig ,
216243 url : string ,
217244) : boolean
218- /**
219- * @deprecated Use the `isFileServingAllowed(config, url)` signature instead.
220- */
221245export function isFileServingAllowed (
222246 url : string ,
223247 server : ViteDevServer ,
@@ -259,33 +283,52 @@ export function isFileLoadingAllowed(
259283 return false
260284}
261285
262- export function ensureServingAccess (
286+ export function checkLoadingAccess (
287+ config : ResolvedConfig ,
288+ path : string ,
289+ ) : 'allowed' | 'denied' | 'fallback' {
290+ if ( isFileLoadingAllowed ( config , slash ( path ) ) ) {
291+ return 'allowed'
292+ }
293+ if ( isFileReadable ( path ) ) {
294+ return 'denied'
295+ }
296+ // if the file doesn't exist, we shouldn't restrict this path as it can
297+ // be an API call. Middlewares would issue a 404 if the file isn't handled
298+ return 'fallback'
299+ }
300+
301+ export function checkServingAccess (
263302 url : string ,
264303 server : ViteDevServer ,
265- res : ServerResponse ,
266- next : Connect . NextFunction ,
267- ) : boolean {
304+ ) : 'allowed' | 'denied' | 'fallback' {
268305 if ( isFileServingAllowed ( url , server ) ) {
269- return true
306+ return 'allowed'
270307 }
271308 if ( isFileReadable ( cleanUrl ( url ) ) ) {
272- const urlMessage = `The request url "${ url } " is outside of Vite serving allow list.`
273- const hintMessage = `
309+ return 'denied'
310+ }
311+ // if the file doesn't exist, we shouldn't restrict this path as it can
312+ // be an API call. Middlewares would issue a 404 if the file isn't handled
313+ return 'fallback'
314+ }
315+
316+ export function respondWithAccessDenied (
317+ url : string ,
318+ server : ViteDevServer ,
319+ res : ServerResponse ,
320+ ) : void {
321+ const urlMessage = `The request url "${ url } " is outside of Vite serving allow list.`
322+ const hintMessage = `
274323${ server . config . server . fs . allow . map ( ( i ) => `- ${ i } ` ) . join ( '\n' ) }
275324
276325Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`
277326
278- server . config . logger . error ( urlMessage )
279- server . config . logger . warnOnce ( hintMessage + '\n' )
280- res . statusCode = 403
281- res . write ( renderRestrictedErrorHTML ( urlMessage + '\n' + hintMessage ) )
282- res . end ( )
283- } else {
284- // if the file doesn't exist, we shouldn't restrict this path as it can
285- // be an API call. Middlewares would issue a 404 if the file isn't handled
286- next ( )
287- }
288- return false
327+ server . config . logger . error ( urlMessage )
328+ server . config . logger . warnOnce ( hintMessage + '\n' )
329+ res . statusCode = 403
330+ res . write ( renderRestrictedErrorHTML ( urlMessage + '\n' + hintMessage ) )
331+ res . end ( )
289332}
290333
291334function renderRestrictedErrorHTML ( msg : string ) : string {
0 commit comments