Next.js CVE-2026-44578: WebSocket SSRF on self-hosted Node.js (not Vercel)
Contents
TL;DR
What happened Next.js 13.4.13–15.5.16 and 16.0.0–16.2.5 self-hosted on the built-in Node.js server. CVSS 8.6 HIGH. Vercel-hosted Next.js is not affected
Fix Update to Next.js 15.5.16 or 16.2.5+. To align with the 2026-05-07 cumulative security release, jump to 15.5.18 or 16.2.6
Workaround At the reverse proxy, block “absolute-form request target + Upgrade: websocket”. Also tighten outbound traffic to cloud metadata endpoints and RFC 1918 ranges
What to grep Unexpected outbound HTTP GETs from the Next.js process to 169.254.169.254, metadata.google.internal, RFC 1918, and link-local addresses. Upgrade-handler traffic does not always appear in the regular HTTP access log
Next.js CVE-2026-44578 is an SSRF that affects environments running the built-in Node.js server as a self-hosted origin, not Vercel-hosted Next.js.
SSRF stands for Server-Side Request Forgery: an externally received request triggers the server to make an internal-network HTTP request on the attacker’s behalf.
The GitHub Advisory GHSA-c4j6-fc7j-m34r covers Next.js 13.4.13 to <15.5.16 and 16.0.0 to <16.2.5.
Fixes are in 15.5.16 and 16.2.5, with a CVSS of 8.6.
The Next.js 16.2.6 / 15.5.18 security release on 2026-05-07 also covers this advisory, so for a fresh upgrade jumping to 15.5.18 or 16.2.6 is simpler.
The path is the upgrade handler, not the regular HTTP handler
The entry point is not a normal page request — it’s an HTTP/1.1 WebSocket upgrade request.
When a client sends Connection: Upgrade plus Upgrade: websocket, Node.js routes it to the upgrade handler instead of the regular request handler.
The bug was in how that upgrade handler treated request targets that looked like external URLs.
Per the GitHub Advisory, the safety check that already existed on the regular HTTP path was extended to the WebSocket upgrade path in the fix.
In other words, “only proxy when the router explicitly marked it as a safe external rewrite” now applies to upgrade requests too.
Hadrian’s technical analysis describes how combining an absolute-form request line with the upgrade headers makes the Next.js process send an HTTP GET to an attacker-chosen host.
Reachable ports end up effectively at port 80.
So when cloud metadata endpoints, internal admin UIs, or in-house HTTP APIs are visible from the Next.js server, information that should not be externally reachable comes back as a response.
An actual attack request roughly looks like this.
Normal WebSocket clients use the origin-form (GET /chat HTTP/1.1), but the absolute-form (GET http://...) is a notation that HTTP/1.1 keeps around for proxy-bound requests.
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: app.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
A normal Next.js app should only proxy external URLs allowed by Router rewrite configuration, but the pre-fix upgrade handler skipped this safety check.
The Next.js process became its own pivot, issuing an HTTP GET to whatever host the absolute-form pointed to.
flowchart TD
A["Attacker"] -->|absolute-form + Upgrade: websocket| B["Self-hosted Next.js Node.js server"]
B --> C["Upgrade handler"]
C -->|safety check skipped| D["HTTP GET to the<br/>absolute-form target"]
D --> E["169.254.169.254<br/>cloud metadata"]
D --> F["RFC 1918<br/>internal HTTP APIs / admin UIs"]
D --> G["link-local<br/>k8s API / etcd / Redis"]
E --> H["IAM credentials etc.<br/>returned in the response"]
Internal endpoints typically targeted by SSRF
When SSRF lands, the attack value is decided by what “not directly externally visible, but reachable without authentication from inside” endpoints the Next.js process can hit.
In cloud and container environments, the usual suspects:
| Path | Address example | What it gives |
|---|---|---|
| AWS IMDSv1 | 169.254.169.254/latest/meta-data/ | EC2 role temporary credentials, region info |
| AWS IMDSv2 | same | Requires a PUT to fetch a session token, so a plain GET SSRF can’t reach |
| GCP metadata | metadata.google.internal/computeMetadata/v1/ | Service account tokens (requires Metadata-Flavor: Google header) |
| Azure IMDS | 169.254.169.254/metadata/ | Managed Identity tokens (requires Metadata: true header) |
| Kubernetes API | cluster IP, kubernetes.default.svc | If a Pod’s ServiceAccount token exists, cluster ops |
| Docker socket over TCP | :2375 | Start arbitrary containers, compromise the host |
| Redis | :6379 | Without auth, key reads and CONFIG SET RCE paths |
| etcd | :2379 | Cluster configuration and secrets |
| Internal admin UI | HTTP ports on RFC 1918 | Direct config changes if auth can be bypassed |
GCP and Azure metadata require additional headers, and IMDSv2 requires a PUT, so a single HTTP GET SSRF often cannot reach them.
Even so, AWS IMDSv1, unauthenticated internal endpoints, and the Redis text-protocol RCE path are still realistic.
Start by checking which private network and link-local ranges the Next.js server is allowed to reach in your outbound firewall.
Vercel-hosted Next.js is out of scope
The advisory explicitly states that Vercel-hosted deployments are not affected.
The affected configuration is Next.js running its built-in Node.js server in Docker, VMs, or Kubernetes, with WebSocket upgrade requests arriving from an untrusted network.
This is a different shape than the earlier React2Shell notes.
React2Shell centered on RSC deserialization, and the relevant variable was whether you were on App Router and React 19-class versions.
CVE-2026-44578 is about the Next.js server’s HTTP upgrade path and self-hosted configuration.
I wrote separately about moving a static site from Next.js to Astro, and the same shape shows up here.
A site serving only static HTML from a CDN never exposes this WebSocket upgrade handler in production.
A Next.js app handling SSR, Route Handlers, and WebSockets on the same origin server, on the other hand, needs both framework updates and a check of how the origin is exposed.
Where to stop it on origins that can’t update right away
The fix is to upgrade Next.js.
For 15.x, 15.5.16 or later; for 16.x, 16.2.5 or later.
If you’re already tracking the May cumulative security release, jump to 15.5.18 or 16.2.6.
pnpm up next@16.2.6
To stay on 15.x:
pnpm up next@15.5.18
For origins you can’t update right now, don’t put the Next.js server directly on the internet.
At a reverse proxy or load balancer in front, drop absolute-form request targets.
If you’re not using WebSockets in the app, drop upgrade requests entirely.
If you are, don’t kill Upgrade: websocket wholesale — reject only the combination with an absolute-form request target starting with http:// or https://.
If nginx sits in front, you can check the request URI and drop it.
# /etc/nginx/conf.d/nextjs.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
server_name app.example.com;
# Drop absolute-form request lines
if ($request_uri ~* "^https?://") {
return 400;
}
location / {
proxy_pass http://nextjs_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
Caddy can match on request path the same way.
At the CDN tier (Cloudflare et al.) you’d write a rule that rejects requests whose URL starts with http:// or https://.
In cloud environments, also tighten outbound traffic to the metadata service.
On AWS, requiring IMDSv2 narrows this attack path significantly — the PUT method needed to obtain a session token means a single-GET SSRF cannot pull metadata.
GCP requires the Metadata-Flavor: Google header, so the same SSRF where the attacker can’t control headers is stopped there as well.
That said, if an internal admin UI or legacy HTTP API is reachable on port 80, blocking metadata alone leaves the rest in place.
Look at which private IP, link-local, and Service IP ranges the Next.js process can reach via outbound NetworkPolicy or Security Groups — that’s the faster check.
Upgrade requests don’t show up in regular logs
WebSocket upgrades don’t always land in the same log files as ordinary HTTP requests.
Looking only at the reverse proxy access log and concluding “nothing here” can miss them.
Patterns worth checking:
- Request line: lines starting with
GET http://orGET https://, accompanied by anUpgrade: websocketheader - Outbound from the Next.js process: sudden connections to
169.254.169.254:80,metadata.google.internal:80, RFC 1918 (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), and link-local (169.254.0.0/16,fe80::/10) - ServiceMesh / k8s paths: externally originated traffic toward intra-cluster Service IPs, especially anomalous hits on the control plane
To pull absolute-form requests out of nginx access logs:
# Extract absolute-form request lines
zgrep -E '"(GET|POST|HEAD) https?://' /var/log/nginx/access.log* \
| grep -i 'upgrade.*websocket'
In the Next.js app logs too, look at http-proxy-related errors, upgrade handling failures, and unexpected fetches to internal URLs.
For container environments, periodic netstat / ss collection or eBPF-based NetworkPolicy (e.g. cilium) can flag outbound destinations not on a whitelist.
This CVE is not RCE.
Even so, if the server’s IAM role or internal API permissions are broad, an HTTP GET alone can return painfully useful information.
After upgrading Next.js, close the direct origin exposure and the path to metadata as well.
Next.js advisories have stacked up in a short window
Over the half-year from late 2025 to May 2026, serious Next.js advisories have piled up.
- December 2025: React2Shell (RSC deserialization) — A privilege-related bug that fires under specific App Router + React 19 setups. Details in React2Shell notes
- April 2026: UAT-10608 puts React2Shell to operational use — A credential harvesting campaign using the post-disclosure vulnerability
- 2026-05-07: Cumulative security release 16.2.6 / 15.5.18 — A rollup update covering multiple advisories
- 2026-05-14: WebSocket upgrade SSRF (this article)
What they have in common is design-level issues in how framework-internal processing paths handle external input.
RSC deserialization, ordering in authentication logic, HTTP upgrade handling — all of these run in layers outside application code.
To run Next.js self-hosted, you need a setup that continuously follows framework security releases.
Avoiding direct origin exposure and putting a reverse proxy or CDN in front works as insurance that absorbs delays in framework-side patches.
Together with SSR origins tending to become the system’s slowest API, full self-hosting has a high ongoing operational cost.
Alternative routes include Vinext (a Next.js reimplementation on a Vite base for Cloudflare) and Blitz.js (a full-stack toolkit on top of Next.js).
Either is a separate long-term decision; for the near term, staying current with Next.js itself is the highest priority.