Skip to content

Commit 0a3dcf5

Browse files
authored
fix: backport #19782, fs check with svg and relative paths (#19785)
1 parent 07ddc3e commit 0a3dcf5

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'
66

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

64-
if (!id.endsWith('.wasm?init')) {
66+
if (!wasmInitRE.test(id)) {
6567
return
6668
}
6769

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'
@@ -19,7 +20,11 @@ import {
1920
withTrailingSlash,
2021
} from '../../utils'
2122
import { send } from '../send'
22-
import { ERR_LOAD_URL, transformRequest } from '../transformRequest'
23+
import {
24+
ERR_DENIED_ID,
25+
ERR_LOAD_URL,
26+
transformRequest,
27+
} from '../transformRequest'
2328
import { applySourcemapIgnoreList } from '../sourcemap'
2429
import { isHTMLProxy } from '../../plugins/html'
2530
import {
@@ -49,6 +54,22 @@ const trailingQuerySeparatorsRE = /[?&]+$/
4954
const urlRE = /[?&]url\b/
5055
const rawRE = /[?&]raw\b/
5156
const inlineRE = /[?&]inline\b/
57+
const svgRE = /\.svg\b/
58+
59+
function deniedServingAccessForTransform(
60+
url: string,
61+
server: ViteDevServer,
62+
res: ServerResponse,
63+
next: Connect.NextFunction,
64+
) {
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+
)
72+
}
5273

5374
export function transformMiddleware(
5475
server: ViteDevServer,
@@ -177,10 +198,7 @@ export function transformMiddleware(
177198
'',
178199
)
179200
if (
180-
(rawRE.test(urlWithoutTrailingQuerySeparators) ||
181-
urlRE.test(urlWithoutTrailingQuerySeparators) ||
182-
inlineRE.test(urlWithoutTrailingQuerySeparators)) &&
183-
!ensureServingAccess(
201+
deniedServingAccessForTransform(
184202
urlWithoutTrailingQuerySeparators,
185203
server,
186204
res,
@@ -227,6 +245,9 @@ export function transformMiddleware(
227245
// resolve, load and transform using the plugin container
228246
const result = await transformRequest(url, server, {
229247
html: req.headers.accept?.includes('text/html'),
248+
allowId(id) {
249+
return !deniedServingAccessForTransform(id, server, res, next)
250+
},
230251
})
231252
if (result) {
232253
const depsOptimizer = getDepsOptimizer(server.config, false) // non-ssr
@@ -288,6 +309,10 @@ export function transformMiddleware(
288309
// Let other middleware handle if we can't load the url via transformRequest
289310
return next()
290311
}
312+
if (e?.code === ERR_DENIED_ID) {
313+
// next() is called in ensureServingAccess
314+
return
315+
}
291316
return next(e)
292317
}
293318

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

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

2525
export const ERR_LOAD_URL = 'ERR_LOAD_URL'
2626
export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL'
27+
export const ERR_DENIED_ID = 'ERR_DENIED_ID'
2728

2829
const debugLoad = createDebugger('vite:load')
2930
const debugTransform = createDebugger('vite:transform')
@@ -40,6 +41,10 @@ export interface TransformResult {
4041
export interface TransformOptions {
4142
ssr?: boolean
4243
html?: boolean
44+
/**
45+
* @internal
46+
*/
47+
allowId?: (id: string) => boolean
4348
}
4449

4550
export function transformRequest(
@@ -182,6 +187,12 @@ async function loadAndTransform(
182187

183188
const file = cleanUrl(id)
184189

190+
if (options.allowId && !options.allowId(id)) {
191+
const err: any = new Error(`Denied ID ${id}`)
192+
err.code = ERR_DENIED_ID
193+
throw err
194+
}
195+
185196
let code: string | null = null
186197
let map: SourceDescription['map'] = null
187198

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)