diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 9706c2b49c5972..17676083ad00b6 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -282,8 +282,9 @@ export async function fileToDevUrl( // If is svg and it's inlined in build, also inline it in dev to match // the behaviour in build due to quote handling differences. - if (svgExtRE.test(id)) { - const file = publicFile || cleanUrl(id) + const cleanedId = cleanUrl(id) + if (svgExtRE.test(cleanedId)) { + const file = publicFile || cleanedId const content = await fsp.readFile(file) if (shouldInline(environment, file, id, content, undefined, undefined)) { return assetToDataURL(environment, file, content) diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index ea0b45a4c68a51..3522b2a01aadbe 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -3,6 +3,8 @@ import { fileToUrl } from './asset' const wasmHelperId = '\0vite/wasm-helper.js' +const wasmInitRE = /(? { let result if (url.startsWith('data:')) { @@ -61,7 +63,7 @@ export const wasmHelperPlugin = (): Plugin => { return `export default ${wasmHelperCode}` } - if (!id.endsWith('.wasm?init')) { + if (!wasmInitRE.test(id)) { return } diff --git a/packages/vite/src/node/server/middlewares/transform.ts b/packages/vite/src/node/server/middlewares/transform.ts index 01f4937759fce5..2c5d1ef162317d 100644 --- a/packages/vite/src/node/server/middlewares/transform.ts +++ b/packages/vite/src/node/server/middlewares/transform.ts @@ -1,5 +1,6 @@ import path from 'node:path' import fsp from 'node:fs/promises' +import type { ServerResponse } from 'node:http' import type { Connect } from 'dep-types/connect' import colors from 'picocolors' import type { ExistingRawSourceMap } from 'rollup' @@ -16,7 +17,11 @@ import { removeTimestampQuery, } from '../../utils' import { send } from '../send' -import { ERR_LOAD_URL, transformRequest } from '../transformRequest' +import { + ERR_DENIED_ID, + ERR_LOAD_URL, + transformRequest, +} from '../transformRequest' import { applySourcemapIgnoreList } from '../sourcemap' import { isHTMLProxy } from '../../plugins/html' import { @@ -47,6 +52,22 @@ const trailingQuerySeparatorsRE = /[?&]+$/ const urlRE = /[?&]url\b/ const rawRE = /[?&]raw\b/ const inlineRE = /[?&]inline\b/ +const svgRE = /\.svg\b/ + +function deniedServingAccessForTransform( + url: string, + server: ViteDevServer, + res: ServerResponse, + next: Connect.NextFunction, +) { + return ( + (rawRE.test(url) || + urlRE.test(url) || + inlineRE.test(url) || + svgRE.test(url)) && + !ensureServingAccess(url, server, res, next) + ) +} /** * A middleware that short-circuits the middleware chain to serve cached transformed modules @@ -178,10 +199,7 @@ export function transformMiddleware( '', ) if ( - (rawRE.test(urlWithoutTrailingQuerySeparators) || - urlRE.test(urlWithoutTrailingQuerySeparators) || - inlineRE.test(urlWithoutTrailingQuerySeparators)) && - !ensureServingAccess( + deniedServingAccessForTransform( urlWithoutTrailingQuerySeparators, server, res, @@ -231,6 +249,9 @@ export function transformMiddleware( // resolve, load and transform using the plugin container const result = await transformRequest(environment, url, { html: req.headers.accept?.includes('text/html'), + allowId(id) { + return !deniedServingAccessForTransform(id, server, res, next) + }, }) if (result) { const depsOptimizer = environment.depsOptimizer @@ -301,6 +322,10 @@ export function transformMiddleware( // Let other middleware handle if we can't load the url via transformRequest return next() } + if (e?.code === ERR_DENIED_ID) { + // next() is called in ensureServingAccess + return + } return next(e) } diff --git a/packages/vite/src/node/server/transformRequest.ts b/packages/vite/src/node/server/transformRequest.ts index a7104753b976a4..898d64146fc55c 100644 --- a/packages/vite/src/node/server/transformRequest.ts +++ b/packages/vite/src/node/server/transformRequest.ts @@ -32,6 +32,7 @@ import type { DevEnvironment } from './environment' export const ERR_LOAD_URL = 'ERR_LOAD_URL' export const ERR_LOAD_PUBLIC_URL = 'ERR_LOAD_PUBLIC_URL' +export const ERR_DENIED_ID = 'ERR_DENIED_ID' const debugLoad = createDebugger('vite:load') const debugTransform = createDebugger('vite:transform') @@ -55,6 +56,10 @@ export interface TransformOptions { * @internal */ html?: boolean + /** + * @internal + */ + allowId?: (id: string) => boolean } // TODO: This function could be moved to the DevEnvironment class. @@ -248,6 +253,12 @@ async function loadAndTransform( const moduleGraph = environment.moduleGraph + if (options.allowId && !options.allowId(id)) { + const err: any = new Error(`Denied ID ${id}`) + err.code = ERR_DENIED_ID + throw err + } + let code: string | null = null let map: SourceDescription['map'] = null diff --git a/playground/fs-serve/__tests__/fs-serve.spec.ts b/playground/fs-serve/__tests__/fs-serve.spec.ts index 795c70e5ae201a..d7d3a28a59bb58 100644 --- a/playground/fs-serve/__tests__/fs-serve.spec.ts +++ b/playground/fs-serve/__tests__/fs-serve.spec.ts @@ -79,6 +79,16 @@ describe.runIf(isServe)('main', () => { ).toBe('403') }) + test('unsafe fetch ?.svg?import', async () => { + expect( + await page.textContent('.unsafe-fetch-query-dot-svg-import-status'), + ).toBe('403') + }) + + test('unsafe fetch .svg?import', async () => { + expect(await page.textContent('.unsafe-fetch-svg-status')).toBe('403') + }) + test('safe fs fetch', async () => { expect(await page.textContent('.safe-fs-fetch')).toBe(stringified) expect(await page.textContent('.safe-fs-fetch-status')).toBe('200') @@ -144,6 +154,14 @@ describe.runIf(isServe)('main', () => { ).toBe('403') }) + test('unsafe fs fetch with relative path after query status', async () => { + expect( + await page.textContent( + '.unsafe-fs-fetch-relative-path-after-query-status', + ), + ).toBe('403') + }) + test('nested entry', async () => { expect(await page.textContent('.nested-entry')).toBe('foobar') }) @@ -157,6 +175,12 @@ describe.runIf(isServe)('main', () => { const code = await page.textContent('.unsafe-dotEnV-casing') expect(code === '403' || code === '404').toBeTruthy() }) + + test('denied env with ?.svg?.wasm?init', async () => { + expect( + await page.textContent('.unsafe-dotenv-query-dot-svg-wasm-init'), + ).toBe('403') + }) }) describe('fetch', () => { diff --git a/playground/fs-serve/root/src/index.html b/playground/fs-serve/root/src/index.html index 931ac38d5e749e..23be5e6b1bde17 100644 --- a/playground/fs-serve/root/src/index.html +++ b/playground/fs-serve/root/src/index.html @@ -25,6 +25,8 @@

Unsafe Fetch


 

 

+

+

 
 

Safe /@fs/ Fetch


@@ -49,6 +51,7 @@ 

Unsafe /@fs/ Fetch


 

 

+

 
 

Nested Entry


@@ -56,6 +59,7 @@ 

Nested Entry

Denied


 

+