@@ -7,7 +7,6 @@ import escapeHtml from 'escape-html'
7
7
import type { ViteDevServer } from '../..'
8
8
import { FS_PREFIX } from '../../constants'
9
9
import {
10
- fsPathFromId ,
11
10
fsPathFromUrl ,
12
11
isFileReadable ,
13
12
isImportRequest ,
@@ -26,11 +25,16 @@ import {
26
25
} from '../../../shared/utils'
27
26
28
27
const knownJavascriptExtensionRE = / \. [ t j ] s x ? $ /
28
+ const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
29
29
30
30
const sirvOptions = ( {
31
+ server,
31
32
getHeaders,
33
+ disableFsServeCheck,
32
34
} : {
35
+ server : ViteDevServer
33
36
getHeaders : ( ) => OutgoingHttpHeaders | undefined
37
+ disableFsServeCheck ?: boolean
34
38
} ) : Options => {
35
39
return {
36
40
dev : true ,
@@ -52,6 +56,22 @@ const sirvOptions = ({
52
56
}
53
57
}
54
58
} ,
59
+ shouldServe : disableFsServeCheck
60
+ ? undefined
61
+ : ( filePath ) => {
62
+ const servingAccessResult = checkLoadingAccess ( server , filePath )
63
+ if ( servingAccessResult === 'denied' ) {
64
+ const error : any = new Error ( 'denied access' )
65
+ error . code = ERR_DENIED_FILE
66
+ error . path = filePath
67
+ throw error
68
+ }
69
+ if ( servingAccessResult === 'fallback' ) {
70
+ return false
71
+ }
72
+ servingAccessResult satisfies 'allowed'
73
+ return true
74
+ } ,
55
75
}
56
76
}
57
77
@@ -63,7 +83,9 @@ export function servePublicMiddleware(
63
83
const serve = sirv (
64
84
dir ,
65
85
sirvOptions ( {
86
+ server,
66
87
getHeaders : ( ) => server . config . server . headers ,
88
+ disableFsServeCheck : true ,
67
89
} ) ,
68
90
)
69
91
@@ -104,6 +126,7 @@ export function serveStaticMiddleware(
104
126
const serve = sirv (
105
127
dir ,
106
128
sirvOptions ( {
129
+ server,
107
130
getHeaders : ( ) => server . config . server . headers ,
108
131
} ) ,
109
132
)
@@ -153,16 +176,20 @@ export function serveStaticMiddleware(
153
176
) {
154
177
fileUrl = withTrailingSlash ( fileUrl )
155
178
}
156
- if ( ! ensureServingAccess ( fileUrl , server , res , next ) ) {
157
- return
158
- }
159
-
160
179
if ( redirectedPathname ) {
161
180
url . pathname = encodeURI ( redirectedPathname )
162
181
req . url = url . href . slice ( url . origin . length )
163
182
}
164
183
165
- serve ( req , res , next )
184
+ try {
185
+ serve ( req , res , next )
186
+ } catch ( e ) {
187
+ if ( e && 'code' in e && e . code === ERR_DENIED_FILE ) {
188
+ respondWithAccessDenied ( e . path , server , res )
189
+ return
190
+ }
191
+ throw e
192
+ }
166
193
}
167
194
}
168
195
@@ -171,7 +198,10 @@ export function serveRawFsMiddleware(
171
198
) : Connect . NextHandleFunction {
172
199
const serveFromRoot = sirv (
173
200
'/' ,
174
- sirvOptions ( { getHeaders : ( ) => server . config . server . headers } ) ,
201
+ sirvOptions ( {
202
+ server,
203
+ getHeaders : ( ) => server . config . server . headers ,
204
+ } ) ,
175
205
)
176
206
177
207
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -183,24 +213,20 @@ export function serveRawFsMiddleware(
183
213
// searching based from fs root.
184
214
if ( url . pathname . startsWith ( FS_PREFIX ) ) {
185
215
const pathname = decodeURI ( url . pathname )
186
- // restrict files outside of `fs.allow`
187
- if (
188
- ! ensureServingAccess (
189
- slash ( path . resolve ( fsPathFromId ( pathname ) ) ) ,
190
- server ,
191
- res ,
192
- next ,
193
- )
194
- ) {
195
- return
196
- }
197
-
198
216
let newPathname = pathname . slice ( FS_PREFIX . length )
199
217
if ( isWindows ) newPathname = newPathname . replace ( / ^ [ A - Z ] : / i, '' )
200
-
201
218
url . pathname = encodeURI ( newPathname )
202
219
req . url = url . href . slice ( url . origin . length )
203
- serveFromRoot ( req , res , next )
220
+
221
+ try {
222
+ serveFromRoot ( req , res , next )
223
+ } catch ( e ) {
224
+ if ( e && 'code' in e && e . code === ERR_DENIED_FILE ) {
225
+ respondWithAccessDenied ( e . path , server , res )
226
+ return
227
+ }
228
+ throw e
229
+ }
204
230
} else {
205
231
next ( )
206
232
}
@@ -209,56 +235,85 @@ export function serveRawFsMiddleware(
209
235
210
236
/**
211
237
* Check if the url is allowed to be served, via the `server.fs` config.
238
+ * @deprecated Use the `isFileLoadingAllowed` function instead.
212
239
*/
213
240
export function isFileServingAllowed (
214
241
url : string ,
215
242
server : ViteDevServer ,
216
243
) : boolean {
217
244
if ( ! server . config . server . fs . strict ) return true
218
245
219
- const file = fsPathFromUrl ( url )
246
+ const filePath = fsPathFromUrl ( url )
247
+ return isFileLoadingAllowed ( server , filePath )
248
+ }
249
+
250
+ function isUriInFilePath ( uri : string , filePath : string ) {
251
+ return isSameFileUri ( uri , filePath ) || isParentDirectory ( uri , filePath )
252
+ }
253
+
254
+ export function isFileLoadingAllowed (
255
+ server : ViteDevServer ,
256
+ filePath : string ,
257
+ ) : boolean {
258
+ const { fs } = server . config . server
220
259
221
- if ( server . _fsDenyGlob ( file ) ) return false
260
+ if ( ! fs . strict ) return true
222
261
223
- if ( server . moduleGraph . safeModulesPath . has ( file ) ) return true
262
+ if ( server . _fsDenyGlob ( filePath ) ) return false
224
263
225
- if (
226
- server . config . server . fs . allow . some (
227
- ( uri ) => isSameFileUri ( uri , file ) || isParentDirectory ( uri , file ) ,
228
- )
229
- )
230
- return true
264
+ if ( server . moduleGraph . safeModulesPath . has ( filePath ) ) return true
265
+
266
+ if ( fs . allow . some ( ( uri ) => isUriInFilePath ( uri , filePath ) ) ) return true
231
267
232
268
return false
233
269
}
234
270
235
- export function ensureServingAccess (
271
+ export function checkLoadingAccess (
272
+ server : ViteDevServer ,
273
+ path : string ,
274
+ ) : 'allowed' | 'denied' | 'fallback' {
275
+ if ( isFileLoadingAllowed ( server , slash ( path ) ) ) {
276
+ return 'allowed'
277
+ }
278
+ if ( isFileReadable ( path ) ) {
279
+ return 'denied'
280
+ }
281
+ // if the file doesn't exist, we shouldn't restrict this path as it can
282
+ // be an API call. Middlewares would issue a 404 if the file isn't handled
283
+ return 'fallback'
284
+ }
285
+
286
+ export function checkServingAccess (
236
287
url : string ,
237
288
server : ViteDevServer ,
238
- res : ServerResponse ,
239
- next : Connect . NextFunction ,
240
- ) : boolean {
289
+ ) : 'allowed' | 'denied' | 'fallback' {
241
290
if ( isFileServingAllowed ( url , server ) ) {
242
- return true
291
+ return 'allowed'
243
292
}
244
293
if ( isFileReadable ( cleanUrl ( url ) ) ) {
245
- const urlMessage = `The request url "${ url } " is outside of Vite serving allow list.`
246
- const hintMessage = `
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 respondWithAccessDenied (
302
+ url : string ,
303
+ server : ViteDevServer ,
304
+ res : ServerResponse ,
305
+ ) : void {
306
+ const urlMessage = `The request url "${ url } " is outside of Vite serving allow list.`
307
+ const hintMessage = `
247
308
${ server . config . server . fs . allow . map ( ( i ) => `- ${ i } ` ) . join ( '\n' ) }
248
309
249
310
Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`
250
311
251
- server . config . logger . error ( urlMessage )
252
- server . config . logger . warnOnce ( hintMessage + '\n' )
253
- res . statusCode = 403
254
- res . write ( renderRestrictedErrorHTML ( urlMessage + '\n' + hintMessage ) )
255
- res . end ( )
256
- } else {
257
- // if the file doesn't exist, we shouldn't restrict this path as it can
258
- // be an API call. Middlewares would issue a 404 if the file isn't handled
259
- next ( )
260
- }
261
- return false
312
+ server . config . logger . error ( urlMessage )
313
+ server . config . logger . warnOnce ( hintMessage + '\n' )
314
+ res . statusCode = 403
315
+ res . write ( renderRestrictedErrorHTML ( urlMessage + '\n' + hintMessage ) )
316
+ res . end ( )
262
317
}
263
318
264
319
function renderRestrictedErrorHTML ( msg : string ) : string {
0 commit comments