@@ -8,7 +8,6 @@ import type { ViteDevServer } from '../../server'
8
8
import type { ResolvedConfig } from '../../config'
9
9
import { FS_PREFIX } from '../../constants'
10
10
import {
11
- fsPathFromId ,
12
11
fsPathFromUrl ,
13
12
isFileReadable ,
14
13
isImportRequest ,
@@ -27,11 +26,16 @@ import {
27
26
} from '../../../shared/utils'
28
27
29
28
const knownJavascriptExtensionRE = / \. (?: [ t j ] s x ? | [ c m ] [ t j ] s ) $ /
29
+ const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
30
30
31
31
const sirvOptions = ( {
32
+ config,
32
33
getHeaders,
34
+ disableFsServeCheck,
33
35
} : {
36
+ config : ResolvedConfig
34
37
getHeaders : ( ) => OutgoingHttpHeaders | undefined
38
+ disableFsServeCheck ?: boolean
35
39
} ) : Options => {
36
40
return {
37
41
dev : true ,
@@ -53,6 +57,22 @@ const sirvOptions = ({
53
57
}
54
58
}
55
59
} ,
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
+ } ,
56
76
}
57
77
}
58
78
@@ -64,7 +84,9 @@ export function servePublicMiddleware(
64
84
const serve = sirv (
65
85
dir ,
66
86
sirvOptions ( {
87
+ config : server . config ,
67
88
getHeaders : ( ) => server . config . server . headers ,
89
+ disableFsServeCheck : true ,
68
90
} ) ,
69
91
)
70
92
@@ -105,6 +127,7 @@ export function serveStaticMiddleware(
105
127
const serve = sirv (
106
128
dir ,
107
129
sirvOptions ( {
130
+ config : server . config ,
108
131
getHeaders : ( ) => server . config . server . headers ,
109
132
} ) ,
110
133
)
@@ -154,16 +177,20 @@ export function serveStaticMiddleware(
154
177
if ( resolvedPathname . endsWith ( '/' ) && fileUrl [ fileUrl . length - 1 ] !== '/' ) {
155
178
fileUrl = withTrailingSlash ( fileUrl )
156
179
}
157
- if ( ! ensureServingAccess ( fileUrl , server , res , next ) ) {
158
- return
159
- }
160
-
161
180
if ( redirectedPathname ) {
162
181
url . pathname = encodeURI ( redirectedPathname )
163
182
req . url = url . href . slice ( url . origin . length )
164
183
}
165
184
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
+ }
167
194
}
168
195
}
169
196
@@ -172,7 +199,10 @@ export function serveRawFsMiddleware(
172
199
) : Connect . NextHandleFunction {
173
200
const serveFromRoot = sirv (
174
201
'/' ,
175
- sirvOptions ( { getHeaders : ( ) => server . config . server . headers } ) ,
202
+ sirvOptions ( {
203
+ config : server . config ,
204
+ getHeaders : ( ) => server . config . server . headers ,
205
+ } ) ,
176
206
)
177
207
178
208
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -184,24 +214,20 @@ export function serveRawFsMiddleware(
184
214
if ( req . url ! . startsWith ( FS_PREFIX ) ) {
185
215
const url = new URL ( req . url ! , 'http://example.com' )
186
216
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
-
199
217
let newPathname = pathname . slice ( FS_PREFIX . length )
200
218
if ( isWindows ) newPathname = newPathname . replace ( / ^ [ A - Z ] : / i, '' )
201
-
202
219
url . pathname = encodeURI ( newPathname )
203
220
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
+ }
205
231
} else {
206
232
next ( )
207
233
}
@@ -210,14 +236,12 @@ export function serveRawFsMiddleware(
210
236
211
237
/**
212
238
* Check if the url is allowed to be served, via the `server.fs` config.
239
+ * @deprecated Use the `isFileLoadingAllowed` function instead.
213
240
*/
214
241
export function isFileServingAllowed (
215
242
config : ResolvedConfig ,
216
243
url : string ,
217
244
) : boolean
218
- /**
219
- * @deprecated Use the `isFileServingAllowed(config, url)` signature instead.
220
- */
221
245
export function isFileServingAllowed (
222
246
url : string ,
223
247
server : ViteDevServer ,
@@ -259,33 +283,52 @@ export function isFileLoadingAllowed(
259
283
return false
260
284
}
261
285
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 (
263
302
url : string ,
264
303
server : ViteDevServer ,
265
- res : ServerResponse ,
266
- next : Connect . NextFunction ,
267
- ) : boolean {
304
+ ) : 'allowed' | 'denied' | 'fallback' {
268
305
if ( isFileServingAllowed ( url , server ) ) {
269
- return true
306
+ return 'allowed'
270
307
}
271
308
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 = `
274
323
${ server . config . server . fs . allow . map ( ( i ) => `- ${ i } ` ) . join ( '\n' ) }
275
324
276
325
Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`
277
326
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 ( )
289
332
}
290
333
291
334
function renderRestrictedErrorHTML ( msg : string ) : string {
0 commit comments