Next.js: null origin can bypass dev HMR websocket CSRF checks
Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.
Affected versions
Details
## Summary In `next dev`, cross-site protections for internal development endpoints could treat `Origin: null` as a bypass case even when [`allowedDevOrigins`](https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins) is configured. This could allow privacy-sensitive or opaque browser contexts, such as sandboxed documents, to access privileged internal dev-server functionality unexpectedly. ## Impact If a developer visits attacker-controlled content while running an affected `next dev` server with [`allowedDevOrigins`](https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins) configured, attacker-controlled browser code may be able to connect to internal development endpoints and interact with sensitive dev-server functionality that should have remained blocked. This issue affects development mode only. It does not affect `next start`, and it does not expose internal debugging functionality to the network by default. ## Patches Fixed by validating `Origin: null` through the same cross-site origin-allowance checks used for other origins on internal development endpoints. ## Workarounds If upgrade is not immediately possible: - Do not expose `next dev` to untrusted networks. - If you use [`allowedDevOrigins`](https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins), reject requests and websocket upgrades with `Origin: null` for internal dev endpoints at your proxy.
The fix
Allow blocking cross-site dev-only websocket connections from
packages/next/src/server/lib/router-server.ts+3 −3
@@ -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 firstif (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
@@ -31,7 +31,7 @@ function warnOrBlockRequest(return true}-function isInternalDevEndpoint(req: IncomingMessage): boolean {+function isInternalEndpoint(req: IncomingMessage): boolean {if (!req.url) return falsetry {@@ -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 hostif (@@ -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 originconst 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
@@ -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()+})+})+}+})})})
References
- WEBhttps://github.com/vercel/next.js/security/advisories/GHSA-jcc7-9wpm-mj36
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2026-27977
- WEBhttps://github.com/vercel/next.js/commit/862f9b9bb41d235e0d8cf44aa811e7fd118cee2a
- PACKAGEhttps://github.com/vercel/next.js
- WEBhttps://github.com/vercel/next.js/releases/tag/v16.1.7