Skip to content

Commit 766947e

Browse files
authored
fix: backport #19965, check static serve file inside sirv (#19966)
1 parent 731b77d commit 766947e

File tree

6 files changed

+146
-57
lines changed

6 files changed

+146
-57
lines changed

docs/config/server-options.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,12 @@ export default defineConfig({
349349

350350
Blocklist for sensitive files being restricted to be served by Vite dev server. This will have higher priority than [`server.fs.allow`](#server-fs-allow). [picomatch patterns](https://github.com/micromatch/picomatch#globbing-features) are supported.
351351

352+
::: tip NOTE
353+
354+
This blocklist does not apply to [the public directory](/guide/assets.md#the-public-directory). All files in the public directory are served without any filtering, since they are copied directly to the output directory during build.
355+
356+
:::
357+
352358
## server.fs.cachedChecks
353359

354360
- **Type:** `boolean`

packages/vite/src/node/publicUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,8 @@ export {
2020
export { send } from './server/send'
2121
export { createLogger } from './logger'
2222
export { searchForWorkspaceRoot } from './server/searchRoot'
23-
export { isFileServingAllowed } from './server/middlewares/static'
23+
export {
24+
isFileServingAllowed,
25+
isFileLoadingAllowed,
26+
} from './server/middlewares/static'
2427
export { loadEnv, resolveEnvPrefix } from './env'

packages/vite/src/node/server/middlewares/static.ts

Lines changed: 103 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import escapeHtml from 'escape-html'
77
import type { ViteDevServer } from '../..'
88
import { FS_PREFIX } from '../../constants'
99
import {
10-
fsPathFromId,
1110
fsPathFromUrl,
1211
isFileReadable,
1312
isImportRequest,
@@ -26,11 +25,16 @@ import {
2625
} from '../../../shared/utils'
2726

2827
const knownJavascriptExtensionRE = /\.[tj]sx?$/
28+
const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
2929

3030
const sirvOptions = ({
31+
server,
3132
getHeaders,
33+
disableFsServeCheck,
3234
}: {
35+
server: ViteDevServer
3336
getHeaders: () => OutgoingHttpHeaders | undefined
37+
disableFsServeCheck?: boolean
3438
}): Options => {
3539
return {
3640
dev: true,
@@ -52,6 +56,22 @@ const sirvOptions = ({
5256
}
5357
}
5458
},
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+
},
5575
}
5676
}
5777

@@ -63,7 +83,9 @@ export function servePublicMiddleware(
6383
const serve = sirv(
6484
dir,
6585
sirvOptions({
86+
server,
6687
getHeaders: () => server.config.server.headers,
88+
disableFsServeCheck: true,
6789
}),
6890
)
6991

@@ -104,6 +126,7 @@ export function serveStaticMiddleware(
104126
const serve = sirv(
105127
dir,
106128
sirvOptions({
129+
server,
107130
getHeaders: () => server.config.server.headers,
108131
}),
109132
)
@@ -153,16 +176,20 @@ export function serveStaticMiddleware(
153176
) {
154177
fileUrl = withTrailingSlash(fileUrl)
155178
}
156-
if (!ensureServingAccess(fileUrl, server, res, next)) {
157-
return
158-
}
159-
160179
if (redirectedPathname) {
161180
url.pathname = encodeURI(redirectedPathname)
162181
req.url = url.href.slice(url.origin.length)
163182
}
164183

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+
}
166193
}
167194
}
168195

@@ -171,7 +198,10 @@ export function serveRawFsMiddleware(
171198
): Connect.NextHandleFunction {
172199
const serveFromRoot = sirv(
173200
'/',
174-
sirvOptions({ getHeaders: () => server.config.server.headers }),
201+
sirvOptions({
202+
server,
203+
getHeaders: () => server.config.server.headers,
204+
}),
175205
)
176206

177207
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
@@ -183,24 +213,20 @@ export function serveRawFsMiddleware(
183213
// searching based from fs root.
184214
if (url.pathname.startsWith(FS_PREFIX)) {
185215
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-
198216
let newPathname = pathname.slice(FS_PREFIX.length)
199217
if (isWindows) newPathname = newPathname.replace(/^[A-Z]:/i, '')
200-
201218
url.pathname = encodeURI(newPathname)
202219
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+
}
204230
} else {
205231
next()
206232
}
@@ -209,56 +235,85 @@ export function serveRawFsMiddleware(
209235

210236
/**
211237
* Check if the url is allowed to be served, via the `server.fs` config.
238+
* @deprecated Use the `isFileLoadingAllowed` function instead.
212239
*/
213240
export function isFileServingAllowed(
214241
url: string,
215242
server: ViteDevServer,
216243
): boolean {
217244
if (!server.config.server.fs.strict) return true
218245

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
220259

221-
if (server._fsDenyGlob(file)) return false
260+
if (!fs.strict) return true
222261

223-
if (server.moduleGraph.safeModulesPath.has(file)) return true
262+
if (server._fsDenyGlob(filePath)) return false
224263

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
231267

232268
return false
233269
}
234270

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(
236287
url: string,
237288
server: ViteDevServer,
238-
res: ServerResponse,
239-
next: Connect.NextFunction,
240-
): boolean {
289+
): 'allowed' | 'denied' | 'fallback' {
241290
if (isFileServingAllowed(url, server)) {
242-
return true
291+
return 'allowed'
243292
}
244293
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 = `
247308
${server.config.server.fs.allow.map((i) => `- ${i}`).join('\n')}
248309
249310
Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`
250311

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()
262317
}
263318

264319
function renderRestrictedErrorHTML(msg: string): string {

packages/vite/src/node/server/middlewares/transform.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import { ERR_CLOSED_SERVER } from '../pluginContainer'
3939
import { getDepsOptimizer } from '../../optimizer'
4040
import { cleanUrl, unwrapId, withTrailingSlash } from '../../../shared/utils'
4141
import { NULL_BYTE_PLACEHOLDER } from '../../../shared/constants'
42-
import { ensureServingAccess } from './static'
42+
import { checkServingAccess, respondWithAccessDenied } from './static'
4343

4444
const debugCache = createDebugger('vite:cache')
4545

@@ -58,13 +58,24 @@ function deniedServingAccessForTransform(
5858
res: ServerResponse,
5959
next: Connect.NextFunction,
6060
) {
61-
return (
62-
(rawRE.test(url) ||
63-
urlRE.test(url) ||
64-
inlineRE.test(url) ||
65-
svgRE.test(url)) &&
66-
!ensureServingAccess(url, server, res, next)
67-
)
61+
if (
62+
rawRE.test(url) ||
63+
urlRE.test(url) ||
64+
inlineRE.test(url) ||
65+
svgRE.test(url)
66+
) {
67+
const servingAccessResult = checkServingAccess(url, server)
68+
if (servingAccessResult === 'denied') {
69+
respondWithAccessDenied(url, server, res)
70+
return true
71+
}
72+
if (servingAccessResult === 'fallback') {
73+
next()
74+
return true
75+
}
76+
servingAccessResult satisfies 'allowed'
77+
}
78+
return false
6879
}
6980

7081
/**

playground/fs-serve/__tests__/fs-serve.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,4 +465,17 @@ describe.runIf(isServe)('invalid request', () => {
465465
)
466466
expect(response).toContain('HTTP/1.1 400 Bad Request')
467467
})
468+
469+
test('should deny request to denied file when a request has /.', async () => {
470+
const response = await sendRawRequest(viteTestUrl, '/src/dummy.crt/.')
471+
expect(response).toContain('HTTP/1.1 403 Forbidden')
472+
})
473+
474+
test('should deny request with /@fs/ to denied file when a request has /.', async () => {
475+
const response = await sendRawRequest(
476+
viteTestUrl,
477+
path.posix.join('/@fs/', root, 'root/src/dummy.crt/') + '.',
478+
)
479+
expect(response).toContain('HTTP/1.1 403 Forbidden')
480+
})
468481
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secret

0 commit comments

Comments
 (0)