Next.js: null origin can bypass dev HMR websocket CSRF checks
Description
Next.js is a React framework for building full-stack web applications. Starting in version 16.0.1 and prior to version 16.1.7, in next dev, cross-site protection for internal websocket endpoints could treat Origin: null as a bypass case even if allowedDevOrigins is configured, allowing privacy-sensitive/opaque contexts (for example sandboxed documents) to connect unexpectedly. If a dev server is reachable from attacker-controlled content, an attacker may be able to connect to the HMR websocket channel and interact with dev websocket traffic. This affects development mode only. Apps without a configured allowedDevOrigins still allow connections from any origin. The issue is fixed in version 16.1.7 by validating Origin: null through the same cross-site origin-allowance checks used for other origins. If upgrading is not immediately possible, do not expose next dev to untrusted networks and/or block websocket upgrades to /_next/webpack-hmr when Origin is null at the proxy.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
nextnpm | >= 16.0.1, < 16.1.7 | 16.1.7 |
Affected products
1Patches
1862f9b9bb41dAllow blocking cross-site dev-only websocket connections from privacy sensitive origins (#91479)
3 files changed · +72 −21
packages/next/src/server/lib/router-server.ts+3 −3 modified@@ -48,7 +48,7 @@ import { normalizedAssetPrefix } from '../../shared/lib/normalized-asset-prefix' import { NEXT_PATCH_SYMBOL } from './patch-fetch' import type { ServerInitResult } from './render-server' import { filterInternalHeaders } from './server-ipc/utils' -import { blockCrossSite } from './router-utils/block-cross-site' +import { blockCrossSiteDEV } from './router-utils/block-cross-site-dev' import { traceGlobals } from '../../trace/shared' import { NoFallbackError } from '../../shared/lib/no-fallback-error.external' import { @@ -355,7 +355,7 @@ export async function initialize(opts: { // handle hot-reloader first if (development) { if ( - blockCrossSite( + blockCrossSiteDEV( req, res, development.config.allowedDevOrigins, @@ -815,7 +815,7 @@ export async function initialize(opts: { if (opts.dev && development && req.url) { if ( - blockCrossSite( + blockCrossSiteDEV( req, socket, development.config.allowedDevOrigins,
packages/next/src/server/lib/router-utils/block-cross-site-dev.ts+19 −18 renamed@@ -31,7 +31,7 @@ function warnOrBlockRequest( return true } -function isInternalDevEndpoint(req: IncomingMessage): boolean { +function isInternalEndpoint(req: IncomingMessage): boolean { if (!req.url) return false try { @@ -50,7 +50,7 @@ function isInternalDevEndpoint(req: IncomingMessage): boolean { } } -export const blockCrossSite = ( +export const blockCrossSiteDEV = ( req: IncomingMessage, res: ServerResponse | Duplex, allowedDevOrigins: string[] | undefined, @@ -70,9 +70,10 @@ export const blockCrossSite = ( } // only process internal URLs/middleware - if (!isInternalDevEndpoint(req)) { + if (!isInternalEndpoint(req)) { return false } + // block non-cors request from cross-site e.g. script tag on // different host if ( @@ -82,20 +83,20 @@ export const blockCrossSite = ( return warnOrBlockRequest(res, undefined, mode) } - // ensure websocket requests from allowed origin + // ensure websocket requests are only fulfilled from allowed origin const rawOrigin = req.headers['origin'] - - if (rawOrigin && rawOrigin !== 'null') { - const parsedOrigin = parseUrl(rawOrigin) - - if (parsedOrigin) { - const originLowerCase = parsedOrigin.hostname.toLowerCase() - - if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) { - return warnOrBlockRequest(res, originLowerCase, mode) - } - } - } - - return false + const parsedOrigin = + rawOrigin && rawOrigin !== 'null' ? parseUrl(rawOrigin) : rawOrigin + + const originLowerCase = + parsedOrigin === undefined || typeof parsedOrigin === 'string' + ? parsedOrigin + : parsedOrigin.hostname.toLowerCase() + + // Allow requests with no origin since those are just GET requests from same-site + return ( + originLowerCase !== undefined && + !isCsrfOriginAllowed(originLowerCase, allowedOrigins) && + warnOrBlockRequest(res, originLowerCase, mode) + ) }
test/development/basic/allowed-dev-origins.test.ts+50 −0 modified@@ -373,6 +373,56 @@ describe.each([['', '/docs']])( server.close() } }) + + it('blocks cross-site requests from privacy-sensitive origins', async () => { + const server = http.createServer((req, res) => { + res.appendHeader('Content-Security-Policy', 'sandbox allow-scripts') + res.end(` + <html> + <head> + <title>testing cross-site privacy-sensitive</title> + </head> + <body> + <script> + (() => { + const statusEl = document.createElement('p') + statusEl.id = 'status' + document.querySelector('body').appendChild(statusEl) + + const ws = new WebSocket("${next.url}/_next/webpack-hmr") + + ws.addEventListener('error', (err) => { + statusEl.innerText = 'error' + }) + ws.addEventListener('open', () => { + statusEl.innerText = 'connected' + }) + })() + </script> + </body> + </html> + `) + }) + + const port = await findPort() + await new Promise<void>((res) => { + server.listen(port, () => res()) + }) + + try { + const browser = await webdriver(`http://127.0.0.1:${port}`, '/') + + await retry(async () => { + expect(await browser.elementByCss('#status').text()).toBe('error') + }) + } finally { + await new Promise<void>((res) => { + server.close(() => { + res() + }) + }) + } + }) }) } )
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
5- github.com/advisories/GHSA-jcc7-9wpm-mj36ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27977ghsaADVISORY
- github.com/vercel/next.js/commit/862f9b9bb41d235e0d8cf44aa811e7fd118cee2aghsax_refsource_MISCWEB
- github.com/vercel/next.js/releases/tag/v16.1.7ghsax_refsource_MISCWEB
- github.com/vercel/next.js/security/advisories/GHSA-jcc7-9wpm-mj36ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.