Skip to content

Commit 6f01ff4

Browse files
committed
fix: apply fs.strict check to HTML files (#20736)
1 parent bdde0f9 commit 6f01ff4

File tree

9 files changed

+138
-3
lines changed

9 files changed

+138
-3
lines changed

packages/vite/src/node/__tests__/utils.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getServerUrlByHost,
1616
injectQuery,
1717
isFileReadable,
18+
isParentDirectory,
1819
mergeWithDefaults,
1920
normalizePath,
2021
numberToPos,
@@ -44,6 +45,33 @@ describe('bareImportRE', () => {
4445
})
4546
})
4647

48+
describe('isParentDirectory', () => {
49+
const cases = {
50+
'/parent': {
51+
'/parent': false,
52+
'/parenta': false,
53+
'/parent/': true,
54+
'/parent/child': true,
55+
'/parent/child/child2': true,
56+
},
57+
'/parent/': {
58+
'/parent': false,
59+
'/parenta': false,
60+
'/parent/': true,
61+
'/parent/child': true,
62+
'/parent/child/child2': true,
63+
},
64+
}
65+
66+
for (const [parent, children] of Object.entries(cases)) {
67+
for (const [child, expected] of Object.entries(children)) {
68+
test(`isParentDirectory("${parent}", "${child}")`, () => {
69+
expect(isParentDirectory(parent, child)).toBe(expected)
70+
})
71+
}
72+
}
73+
})
74+
4775
describe('injectQuery', () => {
4876
if (isWindows) {
4977
// this test will work incorrectly on unix systems

packages/vite/src/node/preview.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { notFoundMiddleware } from './server/middlewares/notFound'
2626
import { proxyMiddleware } from './server/middlewares/proxy'
2727
import {
2828
getServerUrlByHost,
29+
normalizePath,
2930
resolveHostname,
3031
resolveServerUrls,
3132
setupSIGTERMListener,
@@ -263,7 +264,8 @@ export async function preview(
263264

264265
if (config.appType === 'spa' || config.appType === 'mpa') {
265266
// transform index.html
266-
app.use(indexHtmlMiddleware(distDir, server))
267+
const normalizedDistDir = normalizePath(distDir)
268+
app.use(indexHtmlMiddleware(normalizedDistDir, server))
267269

268270
// handle 404s
269271
app.use(notFoundMiddleware())
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { isUriInFilePath } from '../static'
3+
4+
describe('isUriInFilePath', () => {
5+
const cases = {
6+
'/parent': {
7+
'/parent': true,
8+
'/parenta': false,
9+
'/parent/': true,
10+
'/parent/child': true,
11+
'/parent/child/child2': true,
12+
},
13+
'/parent/': {
14+
'/parent': false,
15+
'/parenta': false,
16+
'/parent/': true,
17+
'/parent/child': true,
18+
'/parent/child/child2': true,
19+
},
20+
}
21+
22+
for (const [parent, children] of Object.entries(cases)) {
23+
for (const [child, expected] of Object.entries(children)) {
24+
test(`isUriInFilePath("${parent}", "${child}")`, () => {
25+
expect(isUriInFilePath(parent, child)).toBe(expected)
26+
})
27+
}
28+
}
29+
})

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
isCSSRequest,
3636
isDevServer,
3737
isJSRequest,
38+
isParentDirectory,
3839
joinUrlSegments,
3940
normalizePath,
4041
processSrcSetSync,
@@ -48,6 +49,7 @@ import {
4849
BasicMinimalPluginContext,
4950
basePluginContextMeta,
5051
} from '../pluginContainer'
52+
import { checkLoadingAccess, respondWithAccessDenied } from './static'
5153

5254
interface AssetNode {
5355
start: number
@@ -454,7 +456,26 @@ export function indexHtmlMiddleware(
454456
if (isDev && url.startsWith(FS_PREFIX)) {
455457
filePath = decodeURIComponent(fsPathFromId(url))
456458
} else {
457-
filePath = path.join(root, decodeURIComponent(url))
459+
filePath = normalizePath(
460+
path.resolve(path.join(root, decodeURIComponent(url))),
461+
)
462+
}
463+
464+
if (isDev) {
465+
const servingAccessResult = checkLoadingAccess(server.config, filePath)
466+
if (servingAccessResult === 'denied') {
467+
return respondWithAccessDenied(filePath, server, res)
468+
}
469+
if (servingAccessResult === 'fallback') {
470+
return next()
471+
}
472+
servingAccessResult satisfies 'allowed'
473+
} else {
474+
// `server.fs` options does not apply to the preview server.
475+
// But we should disallow serving files outside the output directory.
476+
if (!isParentDirectory(root, filePath)) {
477+
return next()
478+
}
458479
}
459480

460481
if (fs.existsSync(filePath)) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export function isFileServingAllowed(
262262
return isFileLoadingAllowed(config, filePath)
263263
}
264264

265-
function isUriInFilePath(uri: string, filePath: string) {
265+
export function isUriInFilePath(uri: string, filePath: string): boolean {
266266
return isSameFileUri(uri, filePath) || isParentDirectory(uri, filePath)
267267
}
268268

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ describe.runIf(isServe)('main', () => {
7676
.toBe('403')
7777
})
7878

79+
test('unsafe HTML fetch', async () => {
80+
await expect
81+
.poll(() => page.textContent('.unsafe-fetch-html'))
82+
.toMatch('403 Restricted')
83+
await expect
84+
.poll(() => page.textContent('.unsafe-fetch-html-status'))
85+
.toBe('403')
86+
})
87+
7988
test('unsafe fetch with special characters (#8498)', async () => {
8089
await expect.poll(() => page.textContent('.unsafe-fetch-8498')).toBe('')
8190
await expect
@@ -536,4 +545,18 @@ describe.runIf(isServe)('invalid request', () => {
536545
)
537546
expect(response).toContain('HTTP/1.1 403 Forbidden')
538547
})
548+
549+
test('should deny request to HTML file outside root by default with relative path', async () => {
550+
const response = await sendRawRequest(viteTestUrl, '/../unsafe.html')
551+
expect(response).toContain('HTTP/1.1 403 Forbidden')
552+
})
553+
})
554+
555+
describe.runIf(!isServe)('preview HTML', () => {
556+
test('unsafe HTML fetch', async () => {
557+
await expect.poll(() => page.textContent('.unsafe-fetch-html')).toBe('')
558+
await expect
559+
.poll(() => page.textContent('.unsafe-fetch-html-status'))
560+
.toBe('404')
561+
})
539562
})

playground/fs-serve/root/src/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ <h2>Safe Fetch Subdirectory</h2>
1919
<h2>Unsafe Fetch</h2>
2020
<pre class="unsafe-fetch-status"></pre>
2121
<pre class="unsafe-fetch"></pre>
22+
<pre class="unsafe-fetch-html-status"></pre>
23+
<pre class="unsafe-fetch-html"></pre>
2224
<pre class="unsafe-fetch-8498-status"></pre>
2325
<pre class="unsafe-fetch-8498"></pre>
2426
<pre class="unsafe-fetch-8498-2-status"></pre>
@@ -39,6 +41,8 @@ <h2>Safe /@fs/ Fetch</h2>
3941
<h2>Unsafe /@fs/ Fetch</h2>
4042
<pre class="unsafe-fs-fetch-status"></pre>
4143
<pre class="unsafe-fs-fetch"></pre>
44+
<pre class="unsafe-fs-fetch-html-status"></pre>
45+
<pre class="unsafe-fs-fetch-html"></pre>
4246
<pre class="unsafe-fs-fetch-raw-status"></pre>
4347
<pre class="unsafe-fs-fetch-raw"></pre>
4448
<pre class="unsafe-fs-fetch-raw-query1-status"></pre>
@@ -149,6 +153,19 @@ <h2>Denied</h2>
149153
console.error(e)
150154
})
151155

156+
// outside of allowed dir, treated as unsafe
157+
fetch(joinUrlSegments(base, '/unsafe.html'))
158+
.then((r) => {
159+
text('.unsafe-fetch-html-status', r.status)
160+
return r.text()
161+
})
162+
.then((data) => {
163+
text('.unsafe-fetch-html', data)
164+
})
165+
.catch((e) => {
166+
console.error(e)
167+
})
168+
152169
// outside of allowed dir with special characters #8498
153170
fetch(joinUrlSegments(base, '/src/%2e%2e%2funsafe%2etxt'))
154171
.then((r) => {
@@ -246,6 +263,19 @@ <h2>Denied</h2>
246263
console.error(e)
247264
})
248265

266+
// not imported before, outside of root, treated as unsafe
267+
fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/unsafe.html'))
268+
.then((r) => {
269+
text('.unsafe-fs-fetch-html-status', r.status)
270+
return r.text()
271+
})
272+
.then((data) => {
273+
text('.unsafe-fs-fetch-html', data)
274+
})
275+
.catch((e) => {
276+
console.error(e)
277+
})
278+
249279
// not imported before, outside of root, treated as unsafe
250280
fetch(
251281
joinUrlSegments(

playground/fs-serve/root/unsafe.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>unsafe</p>

playground/fs-serve/unsafe.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>unsafe outside root</p>

0 commit comments

Comments
 (0)