Skip to content

Commit 84b2b46

Browse files
authored
fix: backport #19782, fs check with svg and relative paths (#19784)
1 parent 712cb71 commit 84b2b46

File tree

6 files changed

+122
-6
lines changed

6 files changed

+122
-6
lines changed

packages/vite/src/node/plugins/wasm.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { fileToUrl } from './asset'
44

55
const wasmHelperId = '\0vite/wasm-helper.js'
66

7+
const wasmInitRE = /(?<![?#].*)\.wasm\?init/
8+
79
const wasmHelper = async (opts = {}, url: string) => {
810
let result
911
if (url.startsWith('data:')) {
@@ -62,7 +64,7 @@ export const wasmHelperPlugin = (config: ResolvedConfig): Plugin => {
6264
return `export default ${wasmHelperCode}`
6365
}
6466

65-
if (!id.endsWith('.wasm?init')) {
67+
if (!wasmInitRE.test(id)) {
6668
return
6769
}
6870

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

+30-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import path from 'node:path'
22
import fsp from 'node:fs/promises'
3+
import type { ServerResponse } from 'node:http'
34
import type { Connect } from 'dep-types/connect'
45
import colors from 'picocolors'
56
import type { ExistingRawSourceMap } from 'rollup'
@@ -16,7 +17,11 @@ import {
1617
removeTimestampQuery,
1718
} from '../../utils'
1819
import { send } from '../send'
19-
import { ERR_LOAD_URL, transformRequest } from '../transformRequest'
20+
import {
21+
ERR_DENIED_ID,
22+
ERR_LOAD_URL,
23+
transformRequest,
24+
} from '../transformRequest'
2025
import { applySourcemapIgnoreList } from '../sourcemap'
2126
import { isHTMLProxy } from '../../plugins/html'
2227
import { DEP_VERSION_RE, FS_PREFIX } from '../../constants'
@@ -45,6 +50,22 @@ const trailingQuerySeparatorsRE = /[?&]+$/
4550
const urlRE = /[?&]url\b/
4651
const rawRE = /[?&]raw\b/
4752
const inlineRE = /[?&]inline\b/
53+
const svgRE = /\.svg\b/
54+
55+
function deniedServingAccessForTransform(
56+
url: string,
57+
server: ViteDevServer,
58+
res: ServerResponse,
59+
next: Connect.NextFunction,
60+
) {
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+
)
68+
}
4869

4970
/**
5071
* A middleware that short-circuits the middleware chain to serve cached transformed modules
@@ -172,10 +193,7 @@ export function transformMiddleware(
172193
'',
173194
)
174195
if (
175-
(rawRE.test(urlWithoutTrailingQuerySeparators) ||
176-
urlRE.test(urlWithoutTrailingQuerySeparators) ||
177-
inlineRE.test(urlWithoutTrailingQuerySeparators)) &&
178-
!ensureServingAccess(
196+
deniedServingAccessForTransform(
179197
urlWithoutTrailingQuerySeparators,
180198
server,
181199
res,
@@ -224,6 +242,9 @@ export function transformMiddleware(
224242
// resolve, load and transform using the plugin container
225243
const result = await transformRequest(url, server, {
226244
html: req.headers.accept?.includes('text/html'),
245+
allowId(id) {
246+
return !deniedServingAccessForTransform(id, server, res, next)
247+
},
227248
})
228249
if (result) {
229250
const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr
@@ -294,6 +315,10 @@ export function transformMiddleware(
294315
// Let other middleware handle if we can't load the url via transformRequest
295316
return next()
296317
}
318+
if (e?.code === ERR_DENIED_ID) {
319+
// next() is called in ensureServingAccess
320+
return
321+
}
297322
return next(e)
298323
}
299324

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

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { throwClosedServerError } from './pluginContainer'
3232

3333
export const ERR_LOAD_URL = 'ERR_LOAD_URL'
3434
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
35+
export const ERR_DENIED_ID = 'ERR_DENIED_ID'
3536

3637
const debugLoad = createDebugger('vite:load')
3738
const debugTransform = createDebugger('vite:transform')
@@ -48,6 +49,10 @@ export interface TransformResult {
4849
export interface TransformOptions {
4950
ssr?: boolean
5051
html?: boolean
52+
/**
53+
* @internal
54+
*/
55+
allowId?: (id: string) => boolean
5156
}
5257

5358
export function transformRequest(
@@ -241,6 +246,12 @@ async function loadAndTransform(
241246

242247
const file = cleanUrl(id)
243248

249+
if (options.allowId && !options.allowId(id)) {
250+
const err: any = new Error(`Denied ID ${id}`)
251+
err.code = ERR_DENIED_ID
252+
throw err
253+
}
254+
244255
let code: string | null = null
245256
let map: SourceDescription['map'] = null
246257

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

+24
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ describe.runIf(isServe)('main', () => {
7979
).toBe('403')
8080
})
8181

82+
test('unsafe fetch ?.svg?import', async () => {
83+
expect(
84+
await page.textContent('.unsafe-fetch-query-dot-svg-import-status'),
85+
).toBe('403')
86+
})
87+
88+
test('unsafe fetch .svg?import', async () => {
89+
expect(await page.textContent('.unsafe-fetch-svg-status')).toBe('403')
90+
})
91+
8292
test('safe fs fetch', async () => {
8393
expect(await page.textContent('.safe-fs-fetch')).toBe(stringified)
8494
expect(await page.textContent('.safe-fs-fetch-status')).toBe('200')
@@ -144,6 +154,14 @@ describe.runIf(isServe)('main', () => {
144154
).toBe('403')
145155
})
146156

157+
test('unsafe fs fetch with relative path after query status', async () => {
158+
expect(
159+
await page.textContent(
160+
'.unsafe-fs-fetch-relative-path-after-query-status',
161+
),
162+
).toBe('403')
163+
})
164+
147165
test('nested entry', async () => {
148166
expect(await page.textContent('.nested-entry')).toBe('foobar')
149167
})
@@ -157,6 +175,12 @@ describe.runIf(isServe)('main', () => {
157175
const code = await page.textContent('.unsafe-dotEnV-casing')
158176
expect(code === '403' || code === '404').toBeTruthy()
159177
})
178+
179+
test('denied env with ?.svg?.wasm?init', async () => {
180+
expect(
181+
await page.textContent('.unsafe-dotenv-query-dot-svg-wasm-init'),
182+
).toBe('403')
183+
})
160184
})
161185

162186
describe('fetch', () => {

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

+51
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ <h2>Unsafe Fetch</h2>
2525
<pre class="unsafe-fetch-8498-2"></pre>
2626
<pre class="unsafe-fetch-import-inline-status"></pre>
2727
<pre class="unsafe-fetch-raw-query-import-status"></pre>
28+
<pre class="unsafe-fetch-query-dot-svg-import-status"></pre>
29+
<pre class="unsafe-fetch-svg-status"></pre>
2830

2931
<h2>Safe /@fs/ Fetch</h2>
3032
<pre class="safe-fs-fetch-status"></pre>
@@ -49,13 +51,15 @@ <h2>Unsafe /@fs/ Fetch</h2>
4951
<pre class="unsafe-fs-fetch-8498-2"></pre>
5052
<pre class="unsafe-fs-fetch-import-inline-status"></pre>
5153
<pre class="unsafe-fs-fetch-import-inline-wasm-init-status"></pre>
54+
<pre class="unsafe-fs-fetch-relative-path-after-query-status"></pre>
5255

5356
<h2>Nested Entry</h2>
5457
<pre class="nested-entry"></pre>
5558

5659
<h2>Denied</h2>
5760
<pre class="unsafe-dotenv"></pre>
5861
<pre class="unsafe-dotEnV-casing"></pre>
62+
<pre class="unsafe-dotenv-query-dot-svg-wasm-init"></pre>
5963

6064
<script type="module">
6165
import '../../entry'
@@ -182,6 +186,24 @@ <h2>Denied</h2>
182186
console.error(e)
183187
})
184188

189+
// outside of allowed dir with .svg query import
190+
fetch(joinUrlSegments(base, '/unsafe.txt?.svg?import'))
191+
.then((r) => {
192+
text('.unsafe-fetch-query-dot-svg-import-status', r.status)
193+
})
194+
.catch((e) => {
195+
console.error(e)
196+
})
197+
198+
// svg outside of allowed dir, treated as unsafe
199+
fetch(joinUrlSegments(base, '/unsafe.svg?import'))
200+
.then((r) => {
201+
text('.unsafe-fetch-svg-status', r.status)
202+
})
203+
.catch((e) => {
204+
console.error(e)
205+
})
206+
185207
// imported before, should be treated as safe
186208
fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/safe.json'))
187209
.then((r) => {
@@ -298,6 +320,21 @@ <h2>Denied</h2>
298320
console.error(e)
299321
})
300322

323+
// outside of root with relative path after query
324+
fetch(
325+
joinUrlSegments(
326+
base,
327+
joinUrlSegments('/@fs/', ROOT) +
328+
'/root/src/?/../../unsafe.txt?import&raw',
329+
),
330+
)
331+
.then((r) => {
332+
text('.unsafe-fs-fetch-relative-path-after-query-status', r.status)
333+
})
334+
.catch((e) => {
335+
console.error(e)
336+
})
337+
301338
// outside root with special characters #8498
302339
fetch(
303340
joinUrlSegments(
@@ -368,6 +405,20 @@ <h2>Denied</h2>
368405
console.error(e)
369406
})
370407

408+
// .env with .svg?.wasm?init
409+
fetch(
410+
joinUrlSegments(
411+
base,
412+
joinUrlSegments('/@fs/', ROOT) + '/root/src/.env?.svg?.wasm?init',
413+
),
414+
)
415+
.then((r) => {
416+
text('.unsafe-dotenv-query-dot-svg-wasm-init', r.status)
417+
})
418+
.catch((e) => {
419+
console.error(e)
420+
})
421+
371422
function text(sel, text) {
372423
document.querySelector(sel).textContent = text
373424
}

playground/fs-serve/root/unsafe.svg

+3
Loading

0 commit comments

Comments
 (0)