Vite bypasses server.fs.deny when using `?raw??`
Description
Vite, a provider of frontend development tooling, has a vulnerability in versions prior to 6.2.3, 6.1.2, 6.0.12, 5.4.15, and 4.5.10. @fs denies access to files outside of Vite serving allow list. Adding ?raw?? or ?import&raw?? to the URL bypasses this limitation and returns the file content if it exists. This bypass exists because trailing separators such as ? are removed in several places, but are not accounted for in query string regexes. The contents of arbitrary files can be returned to the browser. Only apps explicitly exposing the Vite dev server to the network (using --host or server.host config option) are affected. Versions 6.2.3, 6.1.2, 6.0.12, 5.4.15, and 4.5.10 fix the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
vitenpm | >= 6.2.0, < 6.2.3 | 6.2.3 |
vitenpm | >= 6.1.0, < 6.1.2 | 6.1.2 |
vitenpm | >= 6.0.0, < 6.0.12 | 6.0.12 |
vitenpm | >= 5.0.0, < 5.4.15 | 5.4.15 |
vitenpm | < 4.5.10 | 4.5.10 |
Affected products
1Patches
5315695e9d97cfix: backport #19702, fs raw query with query separators (#19704)
3 files changed · +65 −2
packages/vite/src/node/server/middlewares/transform.ts+13 −2 modified@@ -44,6 +44,7 @@ import { ensureServingAccess } from './static' const debugCache = createDebugger('vite:cache') const knownIgnoreList = new Set(['/', '/favicon.ico']) +const trailingQuerySeparatorsRE = /[?&]+$/ export function transformMiddleware( server: ViteDevServer, @@ -167,9 +168,19 @@ export function transformMiddleware( } } + const urlWithoutTrailingQuerySeparators = url.replace( + trailingQuerySeparatorsRE, + '', + ) if ( - (rawRE.test(url) || urlRE.test(url)) && - !ensureServingAccess(url, server, res, next) + (rawRE.test(urlWithoutTrailingQuerySeparators) || + urlRE.test(urlWithoutTrailingQuerySeparators)) && + !ensureServingAccess( + urlWithoutTrailingQuerySeparators, + server, + res, + next, + ) ) { return }
playground/fs-serve/root/src/index.html+38 −0 modified@@ -37,6 +37,10 @@ <h2>Unsafe /@fs/ Fetch</h2> <pre class="unsafe-fs-fetch"></pre> <pre class="unsafe-fs-fetch-raw-status"></pre> <pre class="unsafe-fs-fetch-raw"></pre> +<pre class="unsafe-fs-fetch-raw-query1-status"></pre> +<pre class="unsafe-fs-fetch-raw-query1"></pre> +<pre class="unsafe-fs-fetch-raw-query2-status"></pre> +<pre class="unsafe-fs-fetch-raw-query2"></pre> <pre class="unsafe-fs-fetch-8498-status"></pre> <pre class="unsafe-fs-fetch-8498"></pre> <pre class="unsafe-fs-fetch-8498-2-status"></pre> @@ -209,6 +213,40 @@ <h2>Denied</h2> console.error(e) }) + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw??', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query1-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query1', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw?&', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query2-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query2', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + // outside root with special characters #8498 fetch( joinUrlSegments(
playground/fs-serve/__tests__/fs-serve.spec.ts+14 −0 modified@@ -96,6 +96,20 @@ describe.runIf(isServe)('main', () => { expect(await page.textContent('.unsafe-fs-fetch-raw-status')).toBe('403') }) + test('unsafe fs fetch query 1', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query1')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query1-status')).toBe( + '403', + ) + }) + + test('unsafe fs fetch query 2', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query2')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query2-status')).toBe( + '403', + ) + }) + test('unsafe fs fetch with special characters (#8498)', async () => { expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('') expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
807d7f06d33afix: backport #19702, fs raw query with query separators (#19703)
3 files changed · +65 −2
packages/vite/src/node/server/middlewares/transform.ts+13 −2 modified@@ -41,6 +41,7 @@ import { ensureServingAccess } from './static' const debugCache = createDebugger('vite:cache') const knownIgnoreList = new Set(['/', '/favicon.ico']) +const trailingQuerySeparatorsRE = /[?&]+$/ /** * A middleware that short-circuits the middleware chain to serve cached transformed modules @@ -163,9 +164,19 @@ export function transformMiddleware( warnAboutExplicitPublicPathInUrl(url) } + const urlWithoutTrailingQuerySeparators = url.replace( + trailingQuerySeparatorsRE, + '', + ) if ( - (rawRE.test(url) || urlRE.test(url)) && - !ensureServingAccess(url, server, res, next) + (rawRE.test(urlWithoutTrailingQuerySeparators) || + urlRE.test(urlWithoutTrailingQuerySeparators)) && + !ensureServingAccess( + urlWithoutTrailingQuerySeparators, + server, + res, + next, + ) ) { return }
playground/fs-serve/root/src/index.html+38 −0 modified@@ -37,6 +37,10 @@ <h2>Unsafe /@fs/ Fetch</h2> <pre class="unsafe-fs-fetch"></pre> <pre class="unsafe-fs-fetch-raw-status"></pre> <pre class="unsafe-fs-fetch-raw"></pre> +<pre class="unsafe-fs-fetch-raw-query1-status"></pre> +<pre class="unsafe-fs-fetch-raw-query1"></pre> +<pre class="unsafe-fs-fetch-raw-query2-status"></pre> +<pre class="unsafe-fs-fetch-raw-query2"></pre> <pre class="unsafe-fs-fetch-8498-status"></pre> <pre class="unsafe-fs-fetch-8498"></pre> <pre class="unsafe-fs-fetch-8498-2-status"></pre> @@ -209,6 +213,40 @@ <h2>Denied</h2> console.error(e) }) + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw??', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query1-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query1', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw?&', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query2-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query2', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + // outside root with special characters #8498 fetch( joinUrlSegments(
playground/fs-serve/__tests__/fs-serve.spec.ts+14 −0 modified@@ -96,6 +96,20 @@ describe.runIf(isServe)('main', () => { expect(await page.textContent('.unsafe-fs-fetch-raw-status')).toBe('403') }) + test('unsafe fs fetch query 1', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query1')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query1-status')).toBe( + '403', + ) + }) + + test('unsafe fs fetch query 2', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query2')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query2-status')).toBe( + '403', + ) + }) + test('unsafe fs fetch with special characters (#8498)', async () => { expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('') expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
80381c38d6f0fix: fs raw query with query separators (#19702)
3 files changed · +65 −2
packages/vite/src/node/server/middlewares/transform.ts+13 −2 modified@@ -43,6 +43,7 @@ import { ensureServingAccess } from './static' const debugCache = createDebugger('vite:cache') const knownIgnoreList = new Set(['/', '/favicon.ico']) +const trailingQuerySeparatorsRE = /[?&]+$/ /** * A middleware that short-circuits the middleware chain to serve cached transformed modules @@ -169,9 +170,19 @@ export function transformMiddleware( warnAboutExplicitPublicPathInUrl(url) } + const urlWithoutTrailingQuerySeparators = url.replace( + trailingQuerySeparatorsRE, + '', + ) if ( - (rawRE.test(url) || urlRE.test(url)) && - !ensureServingAccess(url, server, res, next) + (rawRE.test(urlWithoutTrailingQuerySeparators) || + urlRE.test(urlWithoutTrailingQuerySeparators)) && + !ensureServingAccess( + urlWithoutTrailingQuerySeparators, + server, + res, + next, + ) ) { return }
playground/fs-serve/root/src/index.html+38 −0 modified@@ -37,6 +37,10 @@ <h2>Unsafe /@fs/ Fetch</h2> <pre class="unsafe-fs-fetch"></pre> <pre class="unsafe-fs-fetch-raw-status"></pre> <pre class="unsafe-fs-fetch-raw"></pre> +<pre class="unsafe-fs-fetch-raw-query1-status"></pre> +<pre class="unsafe-fs-fetch-raw-query1"></pre> +<pre class="unsafe-fs-fetch-raw-query2-status"></pre> +<pre class="unsafe-fs-fetch-raw-query2"></pre> <pre class="unsafe-fs-fetch-8498-status"></pre> <pre class="unsafe-fs-fetch-8498"></pre> <pre class="unsafe-fs-fetch-8498-2-status"></pre> @@ -209,6 +213,40 @@ <h2>Denied</h2> console.error(e) }) + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw??', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query1-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query1', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw?&', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query2-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query2', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + // outside root with special characters #8498 fetch( joinUrlSegments(
playground/fs-serve/__tests__/fs-serve.spec.ts+14 −0 modified@@ -96,6 +96,20 @@ describe.runIf(isServe)('main', () => { expect(await page.textContent('.unsafe-fs-fetch-raw-status')).toBe('403') }) + test('unsafe fs fetch query 1', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query1')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query1-status')).toBe( + '403', + ) + }) + + test('unsafe fs fetch query 2', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query2')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query2-status')).toBe( + '403', + ) + }) + test('unsafe fs fetch with special characters (#8498)', async () => { expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('') expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
92ca12dc7911fix: fs raw query with query separators (#19702)
3 files changed · +65 −2
packages/vite/src/node/server/middlewares/transform.ts+13 −2 modified@@ -43,6 +43,7 @@ import { ensureServingAccess } from './static' const debugCache = createDebugger('vite:cache') const knownIgnoreList = new Set(['/', '/favicon.ico']) +const trailingQuerySeparatorsRE = /[?&]+$/ /** * A middleware that short-circuits the middleware chain to serve cached transformed modules @@ -169,9 +170,19 @@ export function transformMiddleware( warnAboutExplicitPublicPathInUrl(url) } + const urlWithoutTrailingQuerySeparators = url.replace( + trailingQuerySeparatorsRE, + '', + ) if ( - (rawRE.test(url) || urlRE.test(url)) && - !ensureServingAccess(url, server, res, next) + (rawRE.test(urlWithoutTrailingQuerySeparators) || + urlRE.test(urlWithoutTrailingQuerySeparators)) && + !ensureServingAccess( + urlWithoutTrailingQuerySeparators, + server, + res, + next, + ) ) { return }
playground/fs-serve/root/src/index.html+38 −0 modified@@ -37,6 +37,10 @@ <h2>Unsafe /@fs/ Fetch</h2> <pre class="unsafe-fs-fetch"></pre> <pre class="unsafe-fs-fetch-raw-status"></pre> <pre class="unsafe-fs-fetch-raw"></pre> +<pre class="unsafe-fs-fetch-raw-query1-status"></pre> +<pre class="unsafe-fs-fetch-raw-query1"></pre> +<pre class="unsafe-fs-fetch-raw-query2-status"></pre> +<pre class="unsafe-fs-fetch-raw-query2"></pre> <pre class="unsafe-fs-fetch-8498-status"></pre> <pre class="unsafe-fs-fetch-8498"></pre> <pre class="unsafe-fs-fetch-8498-2-status"></pre> @@ -209,6 +213,40 @@ <h2>Denied</h2> console.error(e) }) + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw??', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query1-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query1', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw?&', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query2-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query2', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + // outside root with special characters #8498 fetch( joinUrlSegments(
playground/fs-serve/__tests__/fs-serve.spec.ts+14 −0 modified@@ -96,6 +96,20 @@ describe.runIf(isServe)('main', () => { expect(await page.textContent('.unsafe-fs-fetch-raw-status')).toBe('403') }) + test('unsafe fs fetch query 1', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query1')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query1-status')).toBe( + '403', + ) + }) + + test('unsafe fs fetch query 2', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query2')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query2-status')).toBe( + '403', + ) + }) + test('unsafe fs fetch with special characters (#8498)', async () => { expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('') expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
f234b5744d8bfix: fs raw query with query separators (#19702)
3 files changed · +65 −2
packages/vite/src/node/server/middlewares/transform.ts+13 −2 modified@@ -43,6 +43,7 @@ import { ensureServingAccess } from './static' const debugCache = createDebugger('vite:cache') const knownIgnoreList = new Set(['/', '/favicon.ico']) +const trailingQuerySeparatorsRE = /[?&]+$/ /** * A middleware that short-circuits the middleware chain to serve cached transformed modules @@ -169,9 +170,19 @@ export function transformMiddleware( warnAboutExplicitPublicPathInUrl(url) } + const urlWithoutTrailingQuerySeparators = url.replace( + trailingQuerySeparatorsRE, + '', + ) if ( - (rawRE.test(url) || urlRE.test(url)) && - !ensureServingAccess(url, server, res, next) + (rawRE.test(urlWithoutTrailingQuerySeparators) || + urlRE.test(urlWithoutTrailingQuerySeparators)) && + !ensureServingAccess( + urlWithoutTrailingQuerySeparators, + server, + res, + next, + ) ) { return }
playground/fs-serve/root/src/index.html+38 −0 modified@@ -37,6 +37,10 @@ <h2>Unsafe /@fs/ Fetch</h2> <pre class="unsafe-fs-fetch"></pre> <pre class="unsafe-fs-fetch-raw-status"></pre> <pre class="unsafe-fs-fetch-raw"></pre> +<pre class="unsafe-fs-fetch-raw-query1-status"></pre> +<pre class="unsafe-fs-fetch-raw-query1"></pre> +<pre class="unsafe-fs-fetch-raw-query2-status"></pre> +<pre class="unsafe-fs-fetch-raw-query2"></pre> <pre class="unsafe-fs-fetch-8498-status"></pre> <pre class="unsafe-fs-fetch-8498"></pre> <pre class="unsafe-fs-fetch-8498-2-status"></pre> @@ -209,6 +213,40 @@ <h2>Denied</h2> console.error(e) }) + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw??', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query1-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query1', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + + fetch( + joinUrlSegments( + base, + joinUrlSegments('/@fs/', ROOT) + '/unsafe.json?import&raw?&', + ), + ) + .then((r) => { + text('.unsafe-fs-fetch-raw-query2-status', r.status) + return r.json() + }) + .then((data) => { + text('.unsafe-fs-fetch-raw-query2', JSON.stringify(data)) + }) + .catch((e) => { + console.error(e) + }) + // outside root with special characters #8498 fetch( joinUrlSegments(
playground/fs-serve/__tests__/fs-serve.spec.ts+14 −0 modified@@ -96,6 +96,20 @@ describe.runIf(isServe)('main', () => { expect(await page.textContent('.unsafe-fs-fetch-raw-status')).toBe('403') }) + test('unsafe fs fetch query 1', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query1')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query1-status')).toBe( + '403', + ) + }) + + test('unsafe fs fetch query 2', async () => { + expect(await page.textContent('.unsafe-fs-fetch-raw-query2')).toBe('') + expect(await page.textContent('.unsafe-fs-fetch-raw-query2-status')).toBe( + '403', + ) + }) + test('unsafe fs fetch with special characters (#8498)', async () => { expect(await page.textContent('.unsafe-fs-fetch-8498')).toBe('') expect(await page.textContent('.unsafe-fs-fetch-8498-status')).toBe('404')
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-x574-m823-4x7wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-30208ghsaADVISORY
- github.com/vitejs/vite/commit/315695e9d97cc6cfa7e6d9e0229fb50cdae3d9f4ghsax_refsource_MISCWEB
- github.com/vitejs/vite/commit/80381c38d6f068b12e6e928cd3c616bd1d64803cghsax_refsource_MISCWEB
- github.com/vitejs/vite/commit/807d7f06d33ab49c48a2a3501da3eea1906c0d41ghsax_refsource_MISCWEB
- github.com/vitejs/vite/commit/92ca12dc79118bf66f2b32ff81ed09e0d0bd07caghsax_refsource_MISCWEB
- github.com/vitejs/vite/commit/f234b5744d8b74c95535a7b82cc88ed2144263c1ghsax_refsource_MISCWEB
- github.com/vitejs/vite/security/advisories/GHSA-x574-m823-4x7wghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.