VYPR
Low severityNVD Advisory· Published Mar 17, 2026· Updated Mar 18, 2026

Next.js: null origin can bypass dev HMR websocket CSRF checks

CVE-2026-27977

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.

PackageAffected versionsPatched versions
nextnpm
>= 16.0.1, < 16.1.716.1.7

Affected products

1

Patches

1
862f9b9bb41d

Allow blocking cross-site dev-only websocket connections from privacy sensitive origins (#91479)

https://github.com/vercel/next.jsZack TannerMar 17, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.