@@ -8,7 +8,6 @@ import type { ViteDevServer } from '../..'
88import { FS_PREFIX } from '../../constants'
99import {
1010 cleanUrl ,
11- fsPathFromId ,
1211 fsPathFromUrl ,
1312 isFileReadable ,
1413 isImportRequest ,
@@ -23,13 +22,18 @@ import {
2322} from '../../utils'
2423
2524const knownJavascriptExtensionRE = / \. [ t j ] s x ? $ /
25+ const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
2626
2727const sirvOptions = ( {
28+ server,
2829 headers,
2930 shouldServe,
31+ disableFsServeCheck,
3032} : {
33+ server : ViteDevServer
3134 headers ?: OutgoingHttpHeaders
3235 shouldServe ?: ( p : string ) => void
36+ disableFsServeCheck ?: boolean
3337} ) : Options => {
3438 return {
3539 dev : true ,
@@ -50,19 +54,40 @@ const sirvOptions = ({
5054 }
5155 }
5256 } ,
53- shouldServe,
57+ shouldServe : disableFsServeCheck
58+ ? shouldServe
59+ : ( filePath ) => {
60+ const servingAccessResult = checkLoadingAccess ( server , filePath )
61+ if ( servingAccessResult === 'denied' ) {
62+ const error : any = new Error ( 'denied access' )
63+ error . code = ERR_DENIED_FILE
64+ error . path = filePath
65+ throw error
66+ }
67+ if ( servingAccessResult === 'fallback' ) {
68+ return false
69+ }
70+ servingAccessResult satisfies 'allowed'
71+ if ( shouldServe ) {
72+ return shouldServe ( filePath )
73+ }
74+ return true
75+ } ,
5476 }
5577}
5678
5779export function servePublicMiddleware (
5880 dir : string ,
81+ server : ViteDevServer ,
5982 headers ?: OutgoingHttpHeaders ,
6083) : Connect . NextHandleFunction {
6184 const serve = sirv (
6285 dir ,
6386 sirvOptions ( {
87+ server,
6488 headers,
6589 shouldServe : ( filePath ) => shouldServeFile ( filePath , dir ) ,
90+ disableFsServeCheck : true ,
6691 } ) ,
6792 )
6893
@@ -83,6 +108,7 @@ export function serveStaticMiddleware(
83108 const serve = sirv (
84109 dir ,
85110 sirvOptions ( {
111+ server,
86112 headers : server . config . server . headers ,
87113 } ) ,
88114 )
@@ -132,16 +158,20 @@ export function serveStaticMiddleware(
132158 ) {
133159 fileUrl = withTrailingSlash ( fileUrl )
134160 }
135- if ( ! ensureServingAccess ( fileUrl , server , res , next ) ) {
136- return
137- }
138-
139161 if ( redirectedPathname ) {
140162 url . pathname = encodeURI ( redirectedPathname )
141163 req . url = url . href . slice ( url . origin . length )
142164 }
143165
144- serve ( req , res , next )
166+ try {
167+ serve ( req , res , next )
168+ } catch ( e ) {
169+ if ( e && 'code' in e && e . code === ERR_DENIED_FILE ) {
170+ respondWithAccessDenied ( e . path , server , res )
171+ return
172+ }
173+ throw e
174+ }
145175 }
146176}
147177
@@ -150,7 +180,10 @@ export function serveRawFsMiddleware(
150180) : Connect . NextHandleFunction {
151181 const serveFromRoot = sirv (
152182 '/' ,
153- sirvOptions ( { headers : server . config . server . headers } ) ,
183+ sirvOptions ( {
184+ server,
185+ headers : server . config . server . headers ,
186+ } ) ,
154187 )
155188
156189 // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -162,24 +195,20 @@ export function serveRawFsMiddleware(
162195 // searching based from fs root.
163196 if ( url . pathname . startsWith ( FS_PREFIX ) ) {
164197 const pathname = decodeURI ( url . pathname )
165- // restrict files outside of `fs.allow`
166- if (
167- ! ensureServingAccess (
168- slash ( path . resolve ( fsPathFromId ( pathname ) ) ) ,
169- server ,
170- res ,
171- next ,
172- )
173- ) {
174- return
175- }
176-
177198 let newPathname = pathname . slice ( FS_PREFIX . length )
178199 if ( isWindows ) newPathname = newPathname . replace ( / ^ [ A - Z ] : / i, '' )
179-
180200 url . pathname = encodeURI ( newPathname )
181201 req . url = url . href . slice ( url . origin . length )
182- serveFromRoot ( req , res , next )
202+
203+ try {
204+ serveFromRoot ( req , res , next )
205+ } catch ( e ) {
206+ if ( e && 'code' in e && e . code === ERR_DENIED_FILE ) {
207+ respondWithAccessDenied ( e . path , server , res )
208+ return
209+ }
210+ throw e
211+ }
183212 } else {
184213 next ( )
185214 }
@@ -188,56 +217,85 @@ export function serveRawFsMiddleware(
188217
189218/**
190219 * Check if the url is allowed to be served, via the `server.fs` config.
220+ * @deprecated Use the `isFileLoadingAllowed` function instead.
191221 */
192222export function isFileServingAllowed (
193223 url : string ,
194224 server : ViteDevServer ,
195225) : boolean {
196226 if ( ! server . config . server . fs . strict ) return true
197227
198- const file = fsPathFromUrl ( url )
228+ const filePath = fsPathFromUrl ( url )
229+ return isFileLoadingAllowed ( server , filePath )
230+ }
231+
232+ function isUriInFilePath ( uri : string , filePath : string ) {
233+ return isSameFileUri ( uri , filePath ) || isParentDirectory ( uri , filePath )
234+ }
235+
236+ export function isFileLoadingAllowed (
237+ server : ViteDevServer ,
238+ filePath : string ,
239+ ) : boolean {
240+ const { fs } = server . config . server
199241
200- if ( server . _fsDenyGlob ( file ) ) return false
242+ if ( ! fs . strict ) return true
201243
202- if ( server . moduleGraph . safeModulesPath . has ( file ) ) return true
244+ if ( server . _fsDenyGlob ( filePath ) ) return false
203245
204- if (
205- server . config . server . fs . allow . some (
206- ( uri ) => isSameFileUri ( uri , file ) || isParentDirectory ( uri , file ) ,
207- )
208- )
209- return true
246+ if ( server . moduleGraph . safeModulesPath . has ( filePath ) ) return true
247+
248+ if ( fs . allow . some ( ( uri ) => isUriInFilePath ( uri , filePath ) ) ) return true
210249
211250 return false
212251}
213252
214- export function ensureServingAccess (
253+ export function checkLoadingAccess (
254+ server : ViteDevServer ,
255+ path : string ,
256+ ) : 'allowed' | 'denied' | 'fallback' {
257+ if ( isFileLoadingAllowed ( server , slash ( path ) ) ) {
258+ return 'allowed'
259+ }
260+ if ( isFileReadable ( path ) ) {
261+ return 'denied'
262+ }
263+ // if the file doesn't exist, we shouldn't restrict this path as it can
264+ // be an API call. Middlewares would issue a 404 if the file isn't handled
265+ return 'fallback'
266+ }
267+
268+ export function checkServingAccess (
215269 url : string ,
216270 server : ViteDevServer ,
217- res : ServerResponse ,
218- next : Connect . NextFunction ,
219- ) : boolean {
271+ ) : 'allowed' | 'denied' | 'fallback' {
220272 if ( isFileServingAllowed ( url , server ) ) {
221- return true
273+ return 'allowed'
222274 }
223275 if ( isFileReadable ( cleanUrl ( url ) ) ) {
224- const urlMessage = `The request url "${ url } " is outside of Vite serving allow list.`
225- const hintMessage = `
276+ return 'denied'
277+ }
278+ // if the file doesn't exist, we shouldn't restrict this path as it can
279+ // be an API call. Middlewares would issue a 404 if the file isn't handled
280+ return 'fallback'
281+ }
282+
283+ export function respondWithAccessDenied (
284+ url : string ,
285+ server : ViteDevServer ,
286+ res : ServerResponse ,
287+ ) : void {
288+ const urlMessage = `The request url "${ url } " is outside of Vite serving allow list.`
289+ const hintMessage = `
226290${ server . config . server . fs . allow . map ( ( i ) => `- ${ i } ` ) . join ( '\n' ) }
227291
228292Refer to docs https://vitejs.dev/config/server-options.html#server-fs-allow for configurations and more details.`
229293
230- server . config . logger . error ( urlMessage )
231- server . config . logger . warnOnce ( hintMessage + '\n' )
232- res . statusCode = 403
233- res . write ( renderRestrictedErrorHTML ( urlMessage + '\n' + hintMessage ) )
234- res . end ( )
235- } else {
236- // if the file doesn't exist, we shouldn't restrict this path as it can
237- // be an API call. Middlewares would issue a 404 if the file isn't handled
238- next ( )
239- }
240- return false
294+ server . config . logger . error ( urlMessage )
295+ server . config . logger . warnOnce ( hintMessage + '\n' )
296+ res . statusCode = 403
297+ res . write ( renderRestrictedErrorHTML ( urlMessage + '\n' + hintMessage ) )
298+ res . end ( )
241299}
242300
243301function renderRestrictedErrorHTML ( msg : string ) : string {
0 commit comments