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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 107 additions & 49 deletions
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

Lines changed: 19 additions & 8 deletions
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

Lines changed: 13 additions & 0 deletions
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
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secret

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy