
Verificador OOB para GHSA-c4j6-fc7j-m34r / CVE-2026-44578 (SSRF por actualización WebSocket en Next.js)
Verificador in-band para GHSA-c4j6-fc7j-m34r / CVE-2026-44578 — Falsificación de peticiones del lado del servidor (SSRF) en Next.js mediante peticiones de upgrade WebSocket.
⚠️ Solo para pruebas de seguridad autorizadas. Eres responsable de asegurarte de que tienes permiso para probar cada objetivo que pases a este script.
| Campo | Valor |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CWE | CWE-918 (SSRF) |
| CVSS v3.1 | 8.6 (Alto) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| Afectado | next >=13.4.13 <15.5.16, >=16.0.0 <16.2.5 |
| Parcheado | 15.5.16, 16.2.5 |
| Commit de corrección | c4f69086 |
| No afectado | Alojado en Vercel; output: "export"; despliegues detrás de un proxy inverso que no reenvía Upgrade |
Un atacante abre una conexión TCP a un proceso Next.js autoalojado y envía un upgrade WebSocket HTTP/1.1 cuya request-URI es una URL absoluta:
GET http://anything/<path> HTTP/1.1
Host: <target>
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
En resolveRoutes, la URL contiene // (como toda URI absoluta), lo que
coincide con la rama de "normalizar barras repetidas". Esa rama retorna
anticipadamente con { finished: true, statusCode: 308, parsedUrl: <mangled> }. El
normalizador colapsa http://host/path en http:/host/path (una sola barra).
En router-server.ts, el manejador de upgrade anterior al parche ignoraba
finished/statusCode y solo comprobaba parsedUrl.protocol. Como el
protocolo sobrevive a la normalización, llamaba a proxyRequest(...).
proxyRequest ejecuta url.format(parsedUrl) sobre la URL alterada, obteniendo
http:/host:port/path. http-proxy analiza ese destino, no encuentra host
(url.parse('http:/...').host === null), y cae a su destino predeterminado:
localhost:80 (o localhost:443 para https).
Entonces, en la práctica, el SSRF permite hacer que Next abra un upgrade WebSocket
al localhost:80 / localhost:443 del propio host de Next.js con una
ruta controlada por el atacante.
El fix (commit c4f69086) hizo que el manejador de upgrade comprobara
finished && !statusCode antes de hacer proxy. El caso de normalización 308 ahora
falla la comprobación !statusCode y el socket se cierra en su lugar.
El proxy nunca alcanza un host externo. Si configuras un interactsh /
Burp Collaborator / webhook canary y esperas que el proceso de Next llame a casa,
no lo hará — la conexión va a localhost en la máquina objetivo.
Por lo tanto, este verificador utiliza una señal in-band leída desde el socket
de upgrade: un servidor vulnerable devuelve un cuerpo de error reconocible, un
servidor parcheado no devuelve nada.
El objetivo del SSRF está restringido pero sigue siendo relevante en despliegues reales:
127.0.0.1:80 o :443 y confían en peticiones originadas desde localhost.127.0.0.1:80 (poco común pero visto).Los endpoints de metadatos de AWS / GCP / Azure (169.254.169.254) no son directamente
alcanzables porque el bug fija el destino a localhost.
Para cada objetivo, el script abre un socket TCP (o TLS) crudo, envía el upgrade manipulado, lee la respuesta y produce dos señales:
verdict — si el bug está presente.impact_confirmed — si el SSRF realmente exfiltró datos
(es decir, un servicio co-ubicado en localhost:80/443 del objetivo respondió
y obtuvimos su respuesta).| Respuesta | Veredicto | impact_confirmed |
|---|---|---|
Contiene Internal Server Error | vulnerable | false — bug probado, pero el proxy no alcanzó nada en localhost |
Empieza con HTTP/1. | vulnerable_proxy_succeeded | true — datos de respuesta reales exfiltrados |
| Vacía / cierre limpio | likely_patched | false — también cubre "no es Next", "el proxy inverso eliminó Upgrade", "Vercel" |
| Idéntica al control sin Upgrade | front_end_intercepts | false — el proxy front-end cortocircuitó ambas sondas; el SSRF nunca llegó a Next |
| Cualquier otra cosa | inconclusive | false |
Cuando impact_confirmed es true, la salida JSON también incluye
upstream_status, upstream_server y upstream_content_type extraídos
de la respuesta filtrada (útil para triaje / redacción de informes).
Por defecto, cada objetivo también recibe una sonda de control con la misma
línea de petición de URI absoluta pero sin cabeceras Upgrade (Connection: close).
Si el front-end devuelve la misma respuesta a ambas sondas (línea de estado + tamaño
dentro de la tolerancia), el front-end del propio host está rechazando la línea de
petición de URI absoluta en sí — nginx 400, Apache 400, borde CDN — y el SSRF nunca
llegó a Next. El veredicto se degrada a front_end_intercepts y la salida JSON
incluye front_end_status y front_end_server extraídos de la respuesta del proxy
para que el operador pueda identificar qué está interceptando.
Esto elimina un falso positivo del mundo real observado cuando Next autoalojado está
detrás de nginx/Apache: esos proxies rechazan la línea de petición GET http:///x HTTP/1.1
de la sonda con un 400 genérico, que el detector anteriormente malinterpretaba como
vulnerable_proxy_succeeded. Pasa --no-control-probe para desactivar esta protección
y ver los veredictos crudos.
# 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 sondea una lista integrada de rutas comunes (módulos de estado de
Apache/nginx, endpoints de salud y métricas, Spring Boot Actuator, Go pprof,
endpoints del daemon de Docker, paneles de administración comunes, archivos de
configuración con fugas, rutas de Elasticsearch, etc.) a través del gadget SSRF.
Por defecto, el modo scan ejecuta una sonda de línea base diferencial adicional
con una ruta aleatoria inexistente por objetivo. Las sondas posteriores se etiquetan
DIFF solo cuando su firma (status, body length) diverge de la línea base — los 404
uniformes de un upstream que "no encontró nada" se marcan como noise y no inflan el
contador de aciertos. Pasa --no-differential para informar de cada sonda que alcanzó
un servicio (comportamiento heredado).
La salida se agrupa por objetivo: