Skip to content

Commit fdb196e

Browse files
authored
fix: backport #19782, fs check with svg and relative paths
1 parent 037f801 commit fdb196e

File tree

7 files changed

+125
-8
lines changed

7 files changed

+125
-8
lines changed

Diff for: packages/vite/src/node/plugins/asset.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,9 @@ export async function fileToDevUrl(
282282

283283
// If is svg and it's inlined in build, also inline it in dev to match
284284
// the behaviour in build due to quote handling differences.
285-
if (svgExtRE.test(id)) {
286-
const file = publicFile || cleanUrl(id)
285+
const cleanedId = cleanUrl(id)
286+
if (svgExtRE.test(cleanedId)) {
287+
const file = publicFile || cleanedId
287288
const content = await fsp.readFile(file)
288289
if (shouldInline(environment, file, id, content, undefined, undefined)) {
289290
return assetToDataURL(environment, file, content)

Diff for: packages/vite/src/node/plugins/wasm.ts

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

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

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

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

Diff for: 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 {
@@ -47,6 +52,22 @@ const trailingQuerySeparatorsRE = /[?&]+$/
4752
const urlRE = /[?&]url\b/
4853
const rawRE = /[?&]raw\b/
4954
const inlineRE = /[?&]inline\b/
55+
const svgRE = /\.svg\b/
56+
57+
function deniedServingAccessForTransform(
58+
url: string,
59+
server: ViteDevServer,
60+
res: ServerResponse,
61+
next: Connect.NextFunction,
62+
) {
63+
return (
64+
(rawRE.test(url) ||
65+
urlRE.test(url) ||
66+
inlineRE.test(url) ||
67+
svgRE.test(url)) &&
68+
!ensureServingAccess(url, server, res, next)
69+
)
70+
}
5071

5172
/**
5273
* A middleware that short-circuits the middleware chain to serve cached transformed modules
@@ -178,10 +199,7 @@ export function transformMiddleware(
178199
'',
179200
)
180201
if (
181-
(rawRE.test(urlWithoutTrailingQuerySeparators) ||
182-
urlRE.test(urlWithoutTrailingQuerySeparators) ||
183-
inlineRE.test(urlWithoutTrailingQuerySeparators)) &&
184-
!ensureServingAccess(
202+
deniedServingAccessForTransform(
185203
urlWithoutTrailingQuerySeparators,
186204
server,
187205
res,
@@ -231,6 +249,9 @@ export function transformMiddleware(
231249
// resolve, load and transform using the plugin container
232250
const result = await transformRequest(environment, url, {
233251
html: req.headers.accept?.includes('text/html'),
252+
allowId(id) {
253+
return !deniedServingAccessForTransform(id, server, res, next)
254+
},
234255
})
235256
if (result) {
236257
const depsOptimizer = environment.depsOptimizer
@@ -301,6 +322,10 @@ export function transformMiddleware(
301322
// Let other middleware handle if we can't load the url via transformRequest
302323
return next()
303324
}
325+
if (e?.code === ERR_DENIED_ID) {
326+
// next() is called in ensureServingAccess
327+
return
328+
}
304329
return next(e)
305330
}
306331

Diff for: packages/vite/src/node/server/transformRequest.ts

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type { DevEnvironment } from './environment'
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')
@@ -55,6 +56,10 @@ export interface TransformOptions {
5556
* @internal
5657
*/
5758
html?: boolean
59+
/**
60+
* @internal
61+
*/
62+
allowId?: (id: string) => boolean
5863
}
5964

6065
// TODO: This function could be moved to the DevEnvironment class.
@@ -248,6 +253,12 @@ async function loadAndTransform(
248253

249254
const moduleGraph = environment.moduleGraph
250255

256+
if (options.allowId && !options.allowId(id)) {
257+
const err: any = new Error(`Denied ID ${id}`)
258+
err.code = ERR_DENIED_ID
259+
throw err
260+
}
261+
251262
let code: string | null = null
252263
let map: SourceDescription['map'] = null
253264

Diff for: 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', () => {

Diff for: 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
}

Diff for: playground/fs-serve/root/unsafe.svg

+3
Loading

0 commit comments

Comments
 (0)