CVE-2026-49993
Description
An incomplete same-origin check in Nuxt dev server allows source code theft when bound to a non-loopback address and an attacker on the same network serves a malicious script tag.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An incomplete same-origin check in Nuxt dev server allows source code theft when bound to a non-loopback address and an attacker on the same network serves a malicious script tag.
Vulnerability
The @nuxt/rspack-builder and @nuxt/webpack-builder packages in Nuxt (versions 3.15.4 to before 3.21.7 and 4.0.0 to before 4.4.7) have an incomplete fix for GHSA-6m52-m754-pw2g [1]. The same-origin check in the dev middleware falls back to allow requests when Sec-Fetch-Site, Origin, and Referer headers are all absent, which can be triggered by a cross-origin attacker [1].
Exploitation
An attacker on the same network can craft a page with ` [1]. Because the dev server is bound to a non-loopback address (--host), Sec-Fetch-Site is not sent, Origin is absent for subresource fetches, and Referer is suppressed, the request bypasses the same-origin check and the attacker can read compiled source via window.webpackChunk*` [1].
Impact
Successful exploitation allows the attacker to steal the compiled source code of the Nuxt application, potentially exposing sensitive logic or secrets embedded in the code. No authentication or user interaction beyond opening the attacker's page in the same browser is required if the developer's browser is on the same network as the dev server [1].
Mitigation
The issue is patched in Nuxt versions 3.21.7 and 4.4.7 [1][4]. Users should upgrade immediately. If upgrading is not possible, avoid binding the dev server to a non-loopback address (do not use --host) or ensure the dev server is only accessible within a trusted network. Chrome 142+ users are also protected by Local Network Access restrictions [1].
- @nuxt/webpack-builder and @nuxt/rspack-builder dev server same-origin check bypassed when Sec-Fetch-Site, Origin, and Referer are all absent (incomplete fix for GHSA-6m52-m754-pw2g)
- fix(rspack,webpack): require loopback host when missing same-origin signals by danielroe · Pull Request #35200 · nuxt/nuxt
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
477187ee4015efix(rspack,webpack): require loopback host when missing same-origin signals (#35200)
3 files changed · +115 −24
packages/webpack/src/utils/same-origin.ts+36 −0 added@@ -0,0 +1,36 @@ +const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']) + +function firstHeader (value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value +} + +function isLoopbackHost (host: string | undefined): boolean { + if (!host) { return false } + const withoutPort = host.replace(/:\d+$/, '') + const hostname = withoutPort.replace(/^\[|\]$/g, '').toLowerCase() + return LOOPBACK_HOSTNAMES.has(hostname) +} + +export function isSameOriginRequest (req: { headers: Record<string, string | string[] | undefined> }): boolean { + const site = firstHeader(req.headers['sec-fetch-site']) + if (site !== undefined) { + return site === 'same-origin' || site === 'none' + } + + const initiator = firstHeader(req.headers.origin) || firstHeader(req.headers.referer) + if (!initiator) { + // A request with no `Sec-Fetch-Site`, `Origin`, or `Referer` is only safe to + // allow when the dev server is loopback-bound: a browser-originated request + // can reach a non-loopback bind (e.g. `nuxt dev --host`) with all three + // headers absent if the attacker page is on a non-trustworthy origin + // (drops `Sec-Fetch-*`), uses a non-CORS `<script>` (no `Origin`), and sets + // `Referrer-Policy: no-referrer` (drops `Referer`). + return isLoopbackHost(firstHeader(req.headers.host)) + } + + try { + return new URL(initiator).host === firstHeader(req.headers.host) + } catch { + return false + } +}
packages/webpack/src/webpack.ts+1 −24 modified@@ -15,6 +15,7 @@ import { DynamicBasePlugin } from './plugins/dynamic-base.ts' import { ChunkErrorPlugin } from './plugins/chunk.ts' import { SSRStylesPlugin } from './plugins/ssr-styles.ts' import { createMFS } from './utils/mfs.ts' +import { isSameOriginRequest } from './utils/same-origin.ts' import { client, server } from './configs/index.ts' import { applyPresets, createWebpackConfigContext } from './utils/config.ts' @@ -169,30 +170,6 @@ function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage }) } -// `Sec-Fetch-Site` is not sent in every context, so fall back to comparing the -// initiator (`Origin` / `Referer`) host against the request's `Host`. -function isSameOriginRequest (req: { headers: Record<string, string | string[] | undefined> }): boolean { - const site = firstHeader(req.headers['sec-fetch-site']) - if (site !== undefined) { - return site === 'same-origin' || site === 'none' - } - - const initiator = firstHeader(req.headers.origin) || firstHeader(req.headers.referer) - if (!initiator) { - return true - } - - try { - return new URL(initiator).host === firstHeader(req.headers.host) - } catch { - return false - } -} - -function firstHeader (value: string | string[] | undefined): string | undefined { - return Array.isArray(value) ? value[0] : value -} - async function compile (compiler: Compiler) { const nuxt = useNuxt()
packages/webpack/test/same-origin.test.ts+78 −0 added@@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' +import { isSameOriginRequest } from '../src/utils/same-origin' + +function req (headers: Record<string, string | string[] | undefined>) { + return { headers } +} + +describe('isSameOriginRequest', () => { + describe('with Sec-Fetch-Site present', () => { + it('allows same-origin', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'same-origin', 'host': 'localhost:3000' }))).toBe(true) + }) + + it('allows direct browser navigation (none)', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'none', 'host': 'localhost:3000' }))).toBe(true) + }) + + it('rejects cross-site', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'cross-site', 'host': 'localhost:3000' }))).toBe(false) + }) + + it('rejects same-site (subdomain)', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'same-site', 'host': 'localhost:3000' }))).toBe(false) + }) + + it('handles array-form header values', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': ['cross-site', 'same-origin'], 'host': 'localhost:3000' }))).toBe(false) + }) + }) + + describe('without Sec-Fetch-Site, with Origin or Referer', () => { + it('allows when Origin host matches Host', () => { + expect(isSameOriginRequest(req({ origin: 'http://192.168.0.31:3000', host: '192.168.0.31:3000' }))).toBe(true) + }) + + it('rejects when Origin host differs from Host', () => { + expect(isSameOriginRequest(req({ origin: 'http://evil.lan', host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('allows when Referer host matches Host', () => { + expect(isSameOriginRequest(req({ referer: 'http://192.168.0.31:3000/some/page', host: '192.168.0.31:3000' }))).toBe(true) + }) + + it('rejects the GHSA-6m52-m754-pw2g shape (LAN attacker, Referer present)', () => { + expect(isSameOriginRequest(req({ referer: 'http://evil.lan/', host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('rejects unparseable initiators', () => { + expect(isSameOriginRequest(req({ origin: 'not a url', host: '192.168.0.31:3000' }))).toBe(false) + }) + }) + + describe('GHSA-x6qj-4h56-5rj5: no Sec-Fetch-Site, no Origin, no Referer', () => { + it('allows when the dev server is loopback-bound (localhost)', () => { + expect(isSameOriginRequest(req({ host: 'localhost:3000' }))).toBe(true) + }) + + it('allows when the dev server is loopback-bound (127.0.0.1)', () => { + expect(isSameOriginRequest(req({ host: '127.0.0.1:3000' }))).toBe(true) + }) + + it('allows when the dev server is loopback-bound (IPv6 ::1)', () => { + expect(isSameOriginRequest(req({ host: '[::1]:3000' }))).toBe(true) + }) + + it('rejects when the dev server is bound to a non-loopback LAN address', () => { + expect(isSameOriginRequest(req({ host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('rejects when the dev server is bound to 0.0.0.0 served via a LAN Host header', () => { + expect(isSameOriginRequest(req({ host: '10.0.0.5:3000' }))).toBe(false) + }) + + it('rejects when the Host header is absent', () => { + expect(isSameOriginRequest(req({}))).toBe(false) + }) + }) +})
e351de943e82fix(rspack,webpack): require loopback host when missing same-origin signals (#35200)
3 files changed · +115 −24
packages/webpack/src/utils/same-origin.ts+36 −0 added@@ -0,0 +1,36 @@ +const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']) + +function firstHeader (value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value +} + +function isLoopbackHost (host: string | undefined): boolean { + if (!host) { return false } + const withoutPort = host.replace(/:\d+$/, '') + const hostname = withoutPort.replace(/^\[|\]$/g, '').toLowerCase() + return LOOPBACK_HOSTNAMES.has(hostname) +} + +export function isSameOriginRequest (req: { headers: Record<string, string | string[] | undefined> }): boolean { + const site = firstHeader(req.headers['sec-fetch-site']) + if (site !== undefined) { + return site === 'same-origin' || site === 'none' + } + + const initiator = firstHeader(req.headers.origin) || firstHeader(req.headers.referer) + if (!initiator) { + // A request with no `Sec-Fetch-Site`, `Origin`, or `Referer` is only safe to + // allow when the dev server is loopback-bound: a browser-originated request + // can reach a non-loopback bind (e.g. `nuxt dev --host`) with all three + // headers absent if the attacker page is on a non-trustworthy origin + // (drops `Sec-Fetch-*`), uses a non-CORS `<script>` (no `Origin`), and sets + // `Referrer-Policy: no-referrer` (drops `Referer`). + return isLoopbackHost(firstHeader(req.headers.host)) + } + + try { + return new URL(initiator).host === firstHeader(req.headers.host) + } catch { + return false + } +}
packages/webpack/src/webpack.ts+1 −24 modified@@ -15,6 +15,7 @@ import { DynamicBasePlugin } from './plugins/dynamic-base.ts' import { ChunkErrorPlugin } from './plugins/chunk.ts' import { SSRStylesPlugin } from './plugins/ssr-styles.ts' import { createMFS } from './utils/mfs.ts' +import { isSameOriginRequest } from './utils/same-origin.ts' import { client, server } from './configs/index.ts' import { applyPresets, createWebpackConfigContext } from './utils/config.ts' @@ -169,30 +170,6 @@ function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage }) } -// `Sec-Fetch-Site` is not sent in every context, so fall back to comparing the -// initiator (`Origin` / `Referer`) host against the request's `Host`. -function isSameOriginRequest (req: { headers: Record<string, string | string[] | undefined> }): boolean { - const site = firstHeader(req.headers['sec-fetch-site']) - if (site !== undefined) { - return site === 'same-origin' || site === 'none' - } - - const initiator = firstHeader(req.headers.origin) || firstHeader(req.headers.referer) - if (!initiator) { - return true - } - - try { - return new URL(initiator).host === firstHeader(req.headers.host) - } catch { - return false - } -} - -function firstHeader (value: string | string[] | undefined): string | undefined { - return Array.isArray(value) ? value[0] : value -} - async function compile (compiler: Compiler) { const nuxt = useNuxt()
packages/webpack/test/same-origin.test.ts+78 −0 added@@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' +import { isSameOriginRequest } from '../src/utils/same-origin' + +function req (headers: Record<string, string | string[] | undefined>) { + return { headers } +} + +describe('isSameOriginRequest', () => { + describe('with Sec-Fetch-Site present', () => { + it('allows same-origin', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'same-origin', 'host': 'localhost:3000' }))).toBe(true) + }) + + it('allows direct browser navigation (none)', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'none', 'host': 'localhost:3000' }))).toBe(true) + }) + + it('rejects cross-site', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'cross-site', 'host': 'localhost:3000' }))).toBe(false) + }) + + it('rejects same-site (subdomain)', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'same-site', 'host': 'localhost:3000' }))).toBe(false) + }) + + it('handles array-form header values', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': ['cross-site', 'same-origin'], 'host': 'localhost:3000' }))).toBe(false) + }) + }) + + describe('without Sec-Fetch-Site, with Origin or Referer', () => { + it('allows when Origin host matches Host', () => { + expect(isSameOriginRequest(req({ origin: 'http://192.168.0.31:3000', host: '192.168.0.31:3000' }))).toBe(true) + }) + + it('rejects when Origin host differs from Host', () => { + expect(isSameOriginRequest(req({ origin: 'http://evil.lan', host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('allows when Referer host matches Host', () => { + expect(isSameOriginRequest(req({ referer: 'http://192.168.0.31:3000/some/page', host: '192.168.0.31:3000' }))).toBe(true) + }) + + it('rejects the GHSA-6m52-m754-pw2g shape (LAN attacker, Referer present)', () => { + expect(isSameOriginRequest(req({ referer: 'http://evil.lan/', host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('rejects unparseable initiators', () => { + expect(isSameOriginRequest(req({ origin: 'not a url', host: '192.168.0.31:3000' }))).toBe(false) + }) + }) + + describe('GHSA-x6qj-4h56-5rj5: no Sec-Fetch-Site, no Origin, no Referer', () => { + it('allows when the dev server is loopback-bound (localhost)', () => { + expect(isSameOriginRequest(req({ host: 'localhost:3000' }))).toBe(true) + }) + + it('allows when the dev server is loopback-bound (127.0.0.1)', () => { + expect(isSameOriginRequest(req({ host: '127.0.0.1:3000' }))).toBe(true) + }) + + it('allows when the dev server is loopback-bound (IPv6 ::1)', () => { + expect(isSameOriginRequest(req({ host: '[::1]:3000' }))).toBe(true) + }) + + it('rejects when the dev server is bound to a non-loopback LAN address', () => { + expect(isSameOriginRequest(req({ host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('rejects when the dev server is bound to 0.0.0.0 served via a LAN Host header', () => { + expect(isSameOriginRequest(req({ host: '10.0.0.5:3000' }))).toBe(false) + }) + + it('rejects when the Host header is absent', () => { + expect(isSameOriginRequest(req({}))).toBe(false) + }) + }) +})
e1aa468fd835fix(rspack,webpack): require loopback host when missing same-origin signals (#35200)
3 files changed · +115 −24
packages/webpack/src/utils/same-origin.ts+36 −0 added@@ -0,0 +1,36 @@ +const LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']) + +function firstHeader (value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value +} + +function isLoopbackHost (host: string | undefined): boolean { + if (!host) { return false } + const withoutPort = host.replace(/:\d+$/, '') + const hostname = withoutPort.replace(/^\[|\]$/g, '').toLowerCase() + return LOOPBACK_HOSTNAMES.has(hostname) +} + +export function isSameOriginRequest (req: { headers: Record<string, string | string[] | undefined> }): boolean { + const site = firstHeader(req.headers['sec-fetch-site']) + if (site !== undefined) { + return site === 'same-origin' || site === 'none' + } + + const initiator = firstHeader(req.headers.origin) || firstHeader(req.headers.referer) + if (!initiator) { + // A request with no `Sec-Fetch-Site`, `Origin`, or `Referer` is only safe to + // allow when the dev server is loopback-bound: a browser-originated request + // can reach a non-loopback bind (e.g. `nuxt dev --host`) with all three + // headers absent if the attacker page is on a non-trustworthy origin + // (drops `Sec-Fetch-*`), uses a non-CORS `<script>` (no `Origin`), and sets + // `Referrer-Policy: no-referrer` (drops `Referer`). + return isLoopbackHost(firstHeader(req.headers.host)) + } + + try { + return new URL(initiator).host === firstHeader(req.headers.host) + } catch { + return false + } +}
packages/webpack/src/webpack.ts+1 −24 modified@@ -15,6 +15,7 @@ import { DynamicBasePlugin } from './plugins/dynamic-base.ts' import { ChunkErrorPlugin } from './plugins/chunk.ts' import { SSRStylesPlugin } from './plugins/ssr-styles.ts' import { createMFS } from './utils/mfs.ts' +import { isSameOriginRequest } from './utils/same-origin.ts' import { client, server } from './configs/index.ts' import { applyPresets, createWebpackConfigContext } from './utils/config.ts' @@ -167,30 +168,6 @@ function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage }) } -// `Sec-Fetch-Site` is not sent in every context, so fall back to comparing the -// initiator (`Origin` / `Referer`) host against the request's `Host`. -function isSameOriginRequest (req: { headers: Record<string, string | string[] | undefined> }): boolean { - const site = firstHeader(req.headers['sec-fetch-site']) - if (site !== undefined) { - return site === 'same-origin' || site === 'none' - } - - const initiator = firstHeader(req.headers.origin) || firstHeader(req.headers.referer) - if (!initiator) { - return true - } - - try { - return new URL(initiator).host === firstHeader(req.headers.host) - } catch { - return false - } -} - -function firstHeader (value: string | string[] | undefined): string | undefined { - return Array.isArray(value) ? value[0] : value -} - async function compile (compiler: Compiler) { const nuxt = useNuxt()
packages/webpack/test/same-origin.test.ts+78 −0 added@@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' +import { isSameOriginRequest } from '../src/utils/same-origin' + +function req (headers: Record<string, string | string[] | undefined>) { + return { headers } +} + +describe('isSameOriginRequest', () => { + describe('with Sec-Fetch-Site present', () => { + it('allows same-origin', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'same-origin', 'host': 'localhost:3000' }))).toBe(true) + }) + + it('allows direct browser navigation (none)', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'none', 'host': 'localhost:3000' }))).toBe(true) + }) + + it('rejects cross-site', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'cross-site', 'host': 'localhost:3000' }))).toBe(false) + }) + + it('rejects same-site (subdomain)', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': 'same-site', 'host': 'localhost:3000' }))).toBe(false) + }) + + it('handles array-form header values', () => { + expect(isSameOriginRequest(req({ 'sec-fetch-site': ['cross-site', 'same-origin'], 'host': 'localhost:3000' }))).toBe(false) + }) + }) + + describe('without Sec-Fetch-Site, with Origin or Referer', () => { + it('allows when Origin host matches Host', () => { + expect(isSameOriginRequest(req({ origin: 'http://192.168.0.31:3000', host: '192.168.0.31:3000' }))).toBe(true) + }) + + it('rejects when Origin host differs from Host', () => { + expect(isSameOriginRequest(req({ origin: 'http://evil.lan', host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('allows when Referer host matches Host', () => { + expect(isSameOriginRequest(req({ referer: 'http://192.168.0.31:3000/some/page', host: '192.168.0.31:3000' }))).toBe(true) + }) + + it('rejects the GHSA-6m52-m754-pw2g shape (LAN attacker, Referer present)', () => { + expect(isSameOriginRequest(req({ referer: 'http://evil.lan/', host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('rejects unparseable initiators', () => { + expect(isSameOriginRequest(req({ origin: 'not a url', host: '192.168.0.31:3000' }))).toBe(false) + }) + }) + + describe('GHSA-x6qj-4h56-5rj5: no Sec-Fetch-Site, no Origin, no Referer', () => { + it('allows when the dev server is loopback-bound (localhost)', () => { + expect(isSameOriginRequest(req({ host: 'localhost:3000' }))).toBe(true) + }) + + it('allows when the dev server is loopback-bound (127.0.0.1)', () => { + expect(isSameOriginRequest(req({ host: '127.0.0.1:3000' }))).toBe(true) + }) + + it('allows when the dev server is loopback-bound (IPv6 ::1)', () => { + expect(isSameOriginRequest(req({ host: '[::1]:3000' }))).toBe(true) + }) + + it('rejects when the dev server is bound to a non-loopback LAN address', () => { + expect(isSameOriginRequest(req({ host: '192.168.0.31:3000' }))).toBe(false) + }) + + it('rejects when the dev server is bound to 0.0.0.0 served via a LAN Host header', () => { + expect(isSameOriginRequest(req({ host: '10.0.0.5:3000' }))).toBe(false) + }) + + it('rejects when the Host header is absent', () => { + expect(isSameOriginRequest(req({}))).toBe(false) + }) + }) +})
e763cc34592arefactor(rspack,webpack): extract same-origin check for dev middleware (#35051)
1 file changed · +25 −2
packages/webpack/src/webpack.ts+25 −2 modified@@ -137,9 +137,8 @@ async function createDevMiddleware (compiler: Compiler) { // TODO: implement upstream in `webpack-dev-middleware` function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage, ServerResponse>) { return defineEventHandler(async (event) => { - // disallow cross-site requests in no-cors mode const { req, res } = 'runtime' in event ? event.runtime!.node! : event.node - if (req.headers['sec-fetch-mode'] === 'no-cors' && req.headers['sec-fetch-site'] === 'cross-site') { + if (!isSameOriginRequest(req)) { res!.statusCode = 403 res!.end('Forbidden') return @@ -170,6 +169,30 @@ function wdmToH3Handler (devMiddleware: webpackDevMiddleware.API<IncomingMessage }) } +// `Sec-Fetch-Site` is not sent in every context, so fall back to comparing the +// initiator (`Origin` / `Referer`) host against the request's `Host`. +function isSameOriginRequest (req: { headers: Record<string, string | string[] | undefined> }): boolean { + const site = firstHeader(req.headers['sec-fetch-site']) + if (site !== undefined) { + return site === 'same-origin' || site === 'none' + } + + const initiator = firstHeader(req.headers.origin) || firstHeader(req.headers.referer) + if (!initiator) { + return true + } + + try { + return new URL(initiator).host === firstHeader(req.headers.host) + } catch { + return false + } +} + +function firstHeader (value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value +} + async function compile (compiler: Compiler) { const nuxt = useNuxt()
Vulnerability mechanics
Root cause
"The same-origin check in the dev-middleware unconditionally allowed requests when `Sec-Fetch-Site`, `Origin`, and `Referer` headers were all absent, enabling a cross-origin attacker on the LAN to exfiltrate source code via non-CORS script tags with suppressed referrer."
Attack vector
An attacker on the same LAN as a developer running `nuxt dev --host` can steal the built source code by luring the developer to a malicious webpage. The attacker page loads the dev server's JavaScript bundles via non-CORS `<script>` tags with `referrerpolicy="no-referrer"`. Because the dev server is bound to a non-loopback (LAN) address, the browser does not send `Sec-Fetch-*` headers (non-potentially-trustworthy origin), does not send `Origin` (non-CORS request), and the `Referer` is suppressed by the attacker's policy. The old code accepted such requests unconditionally; the new code rejects them unless the `Host` header is a loopback address.
Affected code
The vulnerability resides in the dev-middleware same-origin check within `packages/webpack/src/webpack.ts` (and the equivalent rspack file). The previous fix (GHSA-6m52-m754-pw2g) added an `Origin`/`Referer` fallback but kept a `return true` branch when all three signal headers (`Sec-Fetch-Site`, `Origin`, `Referer`) were absent. The patch moves the logic into a new shared module `packages/webpack/src/utils/same-origin.ts` and changes that branch to only allow the request when the dev server's `Host` header resolves to a loopback address (`localhost`, `127.0.0.1`, or `::1`).
What the fix does
The patch extracts the inline `isSameOriginRequest` function into a shared module (`packages/webpack/src/utils/same-origin.ts`) and adds a `isLoopbackHost` helper. The critical change is in the branch where `Sec-Fetch-Site`, `Origin`, and `Referer` are all absent: instead of returning `true` unconditionally, the code now calls `isLoopbackHost(firstHeader(req.headers.host))` and only allows the request if the `Host` header is `localhost`, `127.0.0.1`, or `[::1]`. This closes the bypass because a cross-origin attacker on the LAN cannot make the dev server see a loopback `Host` header.
Preconditions
- configThe developer must run the Nuxt dev server with `--host` (or `--host 0.0.0.0`), binding to a non-loopback LAN address.
- inputThe developer must open a malicious webpage served from a different origin on the same local network.
- configThe browser must not enforce Local Network Access restrictions (i.e., must be older than Chrome 142).
Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/nuxt/nuxt/commit/77187ee4015e9267fb464951542a3e09e8b5fa05nvd
- github.com/nuxt/nuxt/commit/e351de943e82db16970618b60dc7fdbaa58630f3nvd
- github.com/nuxt/nuxt/pull/35200nvd
- github.com/nuxt/nuxt/security/advisories/GHSA-6m52-m754-pw2gnvd
- github.com/nuxt/nuxt/security/advisories/GHSA-x6qj-4h56-5rj5nvd
News mentions
0No linked articles in our index yet.