Skip to content

Commit 7739479

Browse files
authored
fix: backport #19965, check static serve file inside sirv (#19967)
1 parent 99afb60 commit 7739479

File tree

8 files changed

+152
-60
lines changed

8 files changed

+152
-60
lines changed

docs/config/server-options.md

+6
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,12 @@ export default defineConfig({
330330

331331
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.
332332

333+
::: tip NOTE
334+
335+
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.
336+
337+
:::
338+
333339
## server.origin
334340

335341
- **Type:** `string`

packages/vite/rollup.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ function createCjsConfig(isProduction: boolean) {
195195
...Object.keys(pkg.dependencies),
196196
...(isProduction ? [] : Object.keys(pkg.devDependencies)),
197197
],
198-
plugins: [...createNodePlugins(false, false, false), bundleSizeLimit(120)],
198+
plugins: [...createNodePlugins(false, false, false), bundleSizeLimit(121)],
199199
})
200200
}
201201

packages/vite/src/node/publicUtils.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ export { normalizePath, mergeConfig, mergeAlias, createFilter } from './utils'
1515
export { send } from './server/send'
1616
export { createLogger } from './logger'
1717
export { searchForWorkspaceRoot } from './server/searchRoot'
18-
export { isFileServingAllowed } from './server/middlewares/static'
18+
export {
19+
isFileServingAllowed,
20+
isFileLoadingAllowed,
21+
} from './server/middlewares/static'
1922
export { loadEnv, resolveEnvPrefix } from './env'

packages/vite/src/node/server/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ export async function _createServer(
668668
// as-is without transforms.
669669
if (config.publicDir) {
670670
middlewares.use(
671-
servePublicMiddleware(config.publicDir, config.server.headers),
671+
servePublicMiddleware(config.publicDir, server, config.server.headers),
672672
)
673673
}
674674

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

+107-49
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { ViteDevServer } from '../..'
88
import { FS_PREFIX } from '../../constants'
99
import {
1010
cleanUrl,
11-
fsPathFromId,
1211
fsPathFromUrl,
1312
isFileReadable,
1413
isImportRequest,
@@ -23,13 +22,18 @@ import {
2322
} from '../../utils'
2423

2524
const knownJavascriptExtensionRE = /\.[tj]sx?$/
25+
const ERR_DENIED_FILE = 'ERR_DENIED_FILE'
2626

2727
const 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

5779
export 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
*/
192222
export 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
228292
Refer 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

243301
function renderRestrictedErrorHTML(msg: string): string {

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

+19-8
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
} from '../../plugins/optimizedDeps'
4444
import { ERR_CLOSED_SERVER } from '../pluginContainer'
4545
import { getDepsOptimizer } from '../../optimizer'
46-
import { ensureServingAccess } from './static'
46+
import { checkServingAccess, respondWithAccessDenied } from './static'
4747

4848
const debugCache = createDebugger('vite:cache')
4949

@@ -62,13 +62,24 @@ function deniedServingAccessForTransform(
6262
res: ServerResponse,
6363
next: Connect.NextFunction,
6464
) {
65-
return (
66-
(rawRE.test(url) ||
67-
urlRE.test(url) ||
68-
inlineRE.test(url) ||
69-
svgRE.test(url)) &&
70-
!ensureServingAccess(url, server, res, next)
71-
)
65+
if (
66+
rawRE.test(url) ||
67+
urlRE.test(url) ||
68+
inlineRE.test(url) ||
69+
svgRE.test(url)
70+
) {
71+
const servingAccessResult = checkServingAccess(url, server)
72+
if (servingAccessResult === 'denied') {
73+
respondWithAccessDenied(url, server, res)
74+
return true
75+
}
76+
if (servingAccessResult === 'fallback') {
77+
next()
78+
return true
79+
}
80+
servingAccessResult satisfies 'allowed'
81+
}
82+
return false
7283
}
7384

7485
export function transformMiddleware(

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

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

0 commit comments

Comments
 (0)