Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/vite/src/node/plugins/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion packages/vite/src/node/plugins/wasm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { fileToUrl } from './asset'

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

const wasmInitRE = /(?<![?#].*)\.wasm\?init/

const wasmHelper = async (opts = {}, url: string) => {
let result
if (url.startsWith('data:')) {
Expand Down Expand Up @@ -61,7 +63,7 @@ export const wasmHelperPlugin = (): Plugin => {
return `export default ${wasmHelperCode}`
}

if (!id.endsWith('.wasm?init')) {
if (!wasmInitRE.test(id)) {
return
}

Expand Down
35 changes: 30 additions & 5 deletions packages/vite/src/node/server/middlewares/transform.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -178,10 +199,7 @@ export function transformMiddleware(
'',
)
if (
(rawRE.test(urlWithoutTrailingQuerySeparators) ||
urlRE.test(urlWithoutTrailingQuerySeparators) ||
inlineRE.test(urlWithoutTrailingQuerySeparators)) &&
!ensureServingAccess(
deniedServingAccessForTransform(
urlWithoutTrailingQuerySeparators,
server,
res,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
11 changes: 11 additions & 0 deletions packages/vite/src/node/server/transformRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions playground/fs-serve/__tests__/fs-serve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
})
Expand All @@ -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', () => {
Expand Down
51 changes: 51 additions & 0 deletions playground/fs-serve/root/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ <h2>Unsafe Fetch</h2>
<pre class="unsafe-fetch-8498-2"></pre>
<pre class="unsafe-fetch-import-inline-status"></pre>
<pre class="unsafe-fetch-raw-query-import-status"></pre>
<pre class="unsafe-fetch-query-dot-svg-import-status"></pre>
<pre class="unsafe-fetch-svg-status"></pre>

<h2>Safe /@fs/ Fetch</h2>
<pre class="safe-fs-fetch-status"></pre>
Expand All @@ -49,13 +51,15 @@ <h2>Unsafe /@fs/ Fetch</h2>
<pre class="unsafe-fs-fetch-8498-2"></pre>
<pre class="unsafe-fs-fetch-import-inline-status"></pre>
<pre class="unsafe-fs-fetch-import-inline-wasm-init-status"></pre>
<pre class="unsafe-fs-fetch-relative-path-after-query-status"></pre>

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

<h2>Denied</h2>
<pre class="unsafe-dotenv"></pre>
<pre class="unsafe-dotEnV-casing"></pre>
<pre class="unsafe-dotenv-query-dot-svg-wasm-init"></pre>

<script type="module">
import '../../entry'
Expand Down Expand Up @@ -182,6 +186,24 @@ <h2>Denied</h2>
console.error(e)
})

// outside of allowed dir with .svg query import
fetch(joinUrlSegments(base, '/unsafe.txt?.svg?import'))
.then((r) => {
text('.unsafe-fetch-query-dot-svg-import-status', r.status)
})
.catch((e) => {
console.error(e)
})

// svg outside of allowed dir, treated as unsafe
fetch(joinUrlSegments(base, '/unsafe.svg?import'))
.then((r) => {
text('.unsafe-fetch-svg-status', r.status)
})
.catch((e) => {
console.error(e)
})

// imported before, should be treated as safe
fetch(joinUrlSegments(base, joinUrlSegments('/@fs/', ROOT) + '/safe.json'))
.then((r) => {
Expand Down Expand Up @@ -298,6 +320,21 @@ <h2>Denied</h2>
console.error(e)
})

// outside of root with relative path after query
fetch(
joinUrlSegments(
base,
joinUrlSegments('/@fs/', ROOT) +
'/root/src/?/../../unsafe.txt?import&raw',
),
)
.then((r) => {
text('.unsafe-fs-fetch-relative-path-after-query-status', r.status)
})
.catch((e) => {
console.error(e)
})

// outside root with special characters #8498
fetch(
joinUrlSegments(
Expand Down Expand Up @@ -368,6 +405,20 @@ <h2>Denied</h2>
console.error(e)
})

// .env with .svg?.wasm?init
fetch(
joinUrlSegments(
base,
joinUrlSegments('/@fs/', ROOT) + '/root/src/.env?.svg?.wasm?init',
),
)
.then((r) => {
text('.unsafe-dotenv-query-dot-svg-wasm-init', r.status)
})
.catch((e) => {
console.error(e)
})

function text(sel, text) {
document.querySelector(sel).textContent = text
}
Expand Down
3 changes: 3 additions & 0 deletions playground/fs-serve/root/unsafe.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading