@@ -8,7 +8,6 @@ import type { ViteDevServer } from '../..'
8
8
import { FS_PREFIX } from '../../constants'
9
9
import {
10
10
cleanUrl ,
11
- fsPathFromId ,
12
11
fsPathFromUrl ,
13
12
isFileReadable ,
14
13
isImportRequest ,
@@ -23,13 +22,18 @@ import {
23
22
} from '../../utils'
24
23
25
24
const knownJavascriptExtensionRE = / \. [ t j ] s x ? $ /
25
+ const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
26
26
27
27
const sirvOptions = ( {
28
+ server,
28
29
headers,
29
30
shouldServe,
31
+ disableFsServeCheck,
30
32
} : {
33
+ server : ViteDevServer
31
34
headers ?: OutgoingHttpHeaders
32
35
shouldServe ?: ( p : string ) => void
36
+ disableFsServeCheck ?: boolean
33
37
} ) : Options => {
34
38
return {
35
39
dev : true ,
@@ -50,19 +54,40 @@ const sirvOptions = ({
50
54
}
51
55
}
52
56
} ,
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
+ } ,
54
76
}
55
77
}
56
78
57
79
export function servePublicMiddleware (
58
80
dir : string ,
81
+ server : ViteDevServer ,
59
82
headers ?: OutgoingHttpHeaders ,
60
83
) : Connect . NextHandleFunction {
61
84
const serve = sirv (
62
85
dir ,
63
86
sirvOptions ( {
87
+ server,
64
88
headers,
65
89
shouldServe : ( filePath ) => shouldServeFile ( filePath , dir ) ,
90
+ disableFsServeCheck : true ,
66
91
} ) ,
67
92
)
68
93
@@ -83,6 +108,7 @@ export function serveStaticMiddleware(
83
108
const serve = sirv (
84
109
dir ,
85
110
sirvOptions ( {
111
+ server,
86
112
headers : server . config . server . headers ,
87
113
} ) ,
88
114
)
@@ -132,16 +158,20 @@ export function serveStaticMiddleware(
132
158
) {
133
159
fileUrl = withTrailingSlash ( fileUrl )
134
160
}
135
- if ( ! ensureServingAccess ( fileUrl , server , res , next ) ) {
136
- return
137
- }
138
-
139
161
if ( redirectedPathname ) {
140
162
url . pathname = encodeURI ( redirectedPathname )
141
163
req . url = url . href . slice ( url . origin . length )
142
164
}
143
165
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
+ }
145
175
}
146
176
}
147
177
@@ -150,7 +180,10 @@ export function serveRawFsMiddleware(
150
180
) : Connect . NextHandleFunction {
151
181
const serveFromRoot = sirv (
152
182
'/' ,
153
- sirvOptions ( { headers : server . config . server . headers } ) ,
183
+ sirvOptions ( {
184
+ server,
185
+ headers : server . config . server . headers ,
186
+ } ) ,
154
187
)
155
188
156
189
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -162,24 +195,20 @@ export function serveRawFsMiddleware(
162
195
// searching based from fs root.
163
196
if ( url . pathname . startsWith ( FS_PREFIX ) ) {
164
197
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
-
177
198
let newPathname = pathname . slice ( FS_PREFIX . length )
178
199
if ( isWindows ) newPathname = newPathname . replace ( / ^ [ A - Z ] : / i, '' )
179
-
180
200
url . pathname = encodeURI ( newPathname )
181
201
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
+ }
183
212
} else {
184
213
next ( )
185
214
}
@@ -188,56 +217,85 @@ export function serveRawFsMiddleware(
188
217
189
218
/**
190
219
* Check if the url is allowed to be served, via the `server.fs` config.
220
+ * @deprecated Use the `isFileLoadingAllowed` function instead.
191
221
*/
192
222
export function isFileServingAllowed (
193
223
url : string ,
194
224
server : ViteDevServer ,
195
225
) : boolean {
196
226
if ( ! server . config . server . fs . strict ) return true
197
227
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
199
241
200
- if ( server . _fsDenyGlob ( file ) ) return false
242
+ if ( ! fs . strict ) return true
201
243
202
- if ( server . moduleGraph . safeModulesPath . has ( file ) ) return true
244
+ if ( server . _fsDenyGlob ( filePath ) ) return false
203
245
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
210
249
211
250
return false
212
251
}
213
252
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 (
215
269
url : string ,
216
270
server : ViteDevServer ,
217
- res : ServerResponse ,
218
- next : Connect . NextFunction ,
219
- ) : boolean {
271
+ ) : 'allowed' | 'denied' | 'fallback' {
220
272
if ( isFileServingAllowed ( url , server ) ) {
221
- return true
273
+ return 'allowed'
222
274
}
223
275
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 = `
226
290
${ server . config . server . fs . allow . map ( ( i ) => `- ${ i } ` ) . join ( '\n' ) }
227
291
228
292
Refer to docs https://vitejs.dev/config/server-options.html#server-fs-allow for configurations and more details.`
229
293
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 ( )
241
299
}
242
300
243
301
function renderRestrictedErrorHTML ( msg : string ) : string {
0 commit comments