← All Case Studies
CVE-2026-44578 HIGH — CVSS 8.6

Next.js WebSocket Upgrade Handler SSRF

Pre-auth SSRF in self-hosted Next.js WebSocket upgrade handler — reach cloud metadata, internal services, any localhost port.

Affected next.js ≥13.4.13 <15.5.16 and ≥16.0.0 <16.2.5
Patched 15.5.16 / 16.2.5 (May 6, 2026)
Package next.js npm (~22M weekly downloads)
CWE CWE-918 (SSRF)
Vercel-hosted deployments are NOT affected. Only self-hosted Next.js deployments using the built-in Node.js server are vulnerable. If you're on Vercel, you are safe.
// what happened
  • Affected: next.js ≥13.4.13 <15.5.16, ≥16.0.0 <16.2.5 (self-hosted only; Vercel not affected)
  • The vulnerable pattern: absolute-form HTTP request URI in WebSocket upgrade header reaches internal services without routing-layer validation
  • Root cause: router-server.ts in the built-in Node.js server proxies upgrade requests with absolute URLs without validating that the routing layer explicitly authorized the target as a safe external rewrite
  • Fix: commit c4f69086 adds finished+statusCode guard before calling proxyRequest()
  • Impact: Unauthenticated remote access to AWS/GCP metadata (IAM credentials), internal admin panels, Redis, PostgreSQL, any port on localhost. Public PoC available.

The WebSocket upgrade handler in next.js's built-in Node.js server accepts absolute-form HTTP request URIs (e.g. GET http://169.254.169.254/latest/meta-data/) in the upgrade request and forwards them to proxyRequest() without validating that the routing layer explicitly authorized the target as a safe external rewrite.

An unauthenticated attacker sends a crafted HTTP Upgrade header with Connection: Upgrade and an absolute-form URI pointing to a cloud metadata endpoint or internal service — the server acts as a blind proxy.

Real-world impact: on a self-hosted next.js instance running inside a container/pod on AWS/GCP/Azure, a single WebSocket upgrade request to http://169.254.169.254/latest/meta-data/ exfiltrates IAM credentials (AccessKeyId, SecretAccessKey, Token). The same primitive reaches any port on localhost: Redis (:6379), PostgreSQL (:5432), internal admin panels, Kubernetes API.

AWS IMDSv1 is vulnerable. IMDSv2 (HttpTokens=required) is not, since it requires a PUT to mint the token first.


packages/next/src/server/lib/router-server.ts — next.js <15.5.16 / <16.2.5 VULNERABLE: WebSocket upgrade handler proxies absolute-form URIs without routing validation const { matchedOutput, parsedUrl } = await resolveRoutes({ req, res, isUpgradeReq: true, signal: signalFromNodeResponse(socket), }) if (matchedOutput) { return socket.end() } // BUG: parsedUrl.protocol is set when an absolute-form URI is parsed (e.g. "http:") // No check that routing explicitly allowed this as a safe external rewrite // proxyRequest() forwards the request to the attacker-controlled hostname if (parsedUrl.protocol) { return await proxyRequest(req, socket, parsedUrl, head) }
Attack: crafting the malicious upgrade request // Attacker sends this WebSocket upgrade request to the self-hosted Next.js server: // GET http://169.254.169.254/latest/meta-data/ HTTP/1.1 // Host: vulnerable-server.example.com // Connection: Upgrade // Upgrade: websocket // Sec-WebSocket-Version: 13 // ...other headers... // The server: // 1. Receives the upgrade request with absolute-form URI // 2. resolveRoutes() is called with isUpgradeReq: true // 3. parsedUrl.protocol is set to "http:" // 4. No finished+statusCode guard — proxyRequest() is called directly // 5. Server acts as a blind proxy to attacker-controlled host // 6. AWS IMDS responds with IAM credentials (AccessKeyId, SecretAccessKey, Token) // 7. Attacker's WebSocket client receives the metadata response

// commit c4f69086 — 13 lines changed (8 removed, 5 added)
packages/next/src/server/lib/router-server.ts — upstream fix at commit c4f69086 next.js 15.5.16 / 16.2.5 — adds finished + statusCode guard - const { matchedOutput, parsedUrl } = await resolveRoutes({ + const { finished, matchedOutput, parsedUrl, statusCode } = await resolveRoutes({ req, res, isUpgradeReq: true, signal: signalFromNodeResponse(socket), }) if (matchedOutput) { return socket.end() } - if (parsedUrl.protocol) { - return await proxyRequest(req, socket, parsedUrl, head) + if (finished && parsedUrl.protocol) { + if (!statusCode) { + return await proxyRequest(req, socket, parsedUrl, head) + } + return socket.end() }

The fix adds two guards: finished must be true (routing explicitly marked target as a safe external rewrite) AND statusCode must be absent (no redirect/error returned). Only then does it call proxyRequest(). An absolute-form URI that wasn't explicitly rewritten by the routing layer now fails the finished check.


🟠 PullLight — High Finding
[SSRF] Unguarded WebSocket upgrade proxy — packages/next/src/server/lib/router-server.ts:handleUpgrade

parsedUrl.protocol is set from the request-line URI — an attacker-controlled field. Calling proxyRequest(req, socket, parsedUrl, head) without a finished+statusCode routing guard lets any absolute-form URI be proxied server-side. A request like GET http://169.254.169.254/latest/meta-data/ with Upgrade: websocket headers reaches AWS/GCP metadata and returns IAM credentials. This is CWE-918 (SSRF), pre-auth, low complexity. IMDSv1 on AWS is exploitable; IMDSv2 (PUT-required token mint) is not. Internal services on localhost:6379, :5432, :8080 are all reachable. Public PoC: github.com/dinosn/CVE-2026-44578.
```suggestion // Fix: apply the same routing guards as normal HTTP proxy requests const { finished, matchedOutput, parsedUrl, statusCode } = await resolveRoutes({ req, res, isUpgradeReq: true, signal: signalFromNodeResponse(socket), }) if (matchedOutput) return socket.end() // Only proxy when routing explicitly marked the target as a safe external rewrite if (finished && parsedUrl.protocol) { if (!statusCode) { return await proxyRequest(req, socket, parsedUrl, head) } return socket.end() } // Upgrade request to non-rewrite target — reject cleanly ``` Fix: Upgrade to next.js 15.5.16+ / 16.2.5+. If you cannot upgrade immediately, block WebSocket upgrade headers (Connection: Upgrade, Upgrade: websocket) at your reverse proxy or load balancer if WebSockets are not required by your application. Enforce IMDSv2 on cloud VMs (aws ec2 modify-instance-metadata-options --http-tokens required) to prevent metadata credential exfiltration even if the SSRF is exploited.

// why this happened
The WebSocket upgrade handler was introduced in next.js 13.4.13 as part of the built-in Node.js server's WebSocket support. It reuses the existing routing resolution but the old implementation checked only parsedUrl.protocol before calling proxyRequest() — it never validated whether the routing layer had explicitly marked the target as a safe external rewrite. An absolute-form HTTP request URI (e.g. GET http://169.254.169.254/) causes Url.parse() to set parsedUrl.protocol to "http:", triggering the proxy path without any routing-layer authorization. The same bug class (absolute URI SSRF in proxy handling) is a recurring pattern in Node.js frameworks — see CVE-2024-29415 (ip package), CVE-2023-44487 (HTTP/2 Rapid Reset), and CVE-2024-21538 (axios SSRF).

Three reasons this vulnerability hides from standard PR review

  • The bug is in framework internals. router-server.ts is deep in next.js's built-in server code, not in application code. Reviewers trust the framework's internal handling more than their own code.
  • WebSocket upgrade handling looks like infrastructure plumbing. There's no obvious "this is a security check" intent to analyze. The routing function name (resolveRoutes) suggests safety.
  • The absolute-form URI attack surface is invisible without knowing HTTP/1.1 semantics — most developers don't think about the distinction between a relative request-target and an absolute-form URI in a WebSocket upgrade context. The attacker payload (GET http://169.254.169.254/...) looks like a normal HTTP request at first glance.

May 6, 2026 GHSA-c4j6-fc7j-m34r published; next.js 15.5.16 and 16.2.5 released with patch
May 13, 2026 CVE-2026-44578 published on NVD (CVSS 8.6 High)
May 2026 Multiple public PoCs released (dinosn/CVE-2026-44578, dwisiswant0/next-16.2.4-pocs, ynsmroztas/nextssrf)
May 2026 Coordinated security release covering 13 Next.js and React advisories in the same batch
Jun 2026 PullLight documents this as the 9th CVE case study — helps engineers recognize the absolute-URI SSRF pattern in framework-level code

Don't let this happen to your PRs

More Case Studies
10.0React2Shell RCE (CVE-2025-55182)
10.0vm2 Sandbox Escape (CVE-2026-44005)
9.2jsPDF Path Traversal (CVE-2025-68428)
9.1Waitress TOCTOU Race (CVE-2024-49768)
9.0jsonpath-plus RCE (CVE-2024-21534)
8.1ip Package SSRF (CVE-2024-29415)
7.2Next.js Auth Bypass (CVE-2025-29927)