
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 :