
Verificatore OOB per GHSA-c4j6-fc7j-m34r / CVE-2026-44578 (Next.js WebSocket-upgrade SSRF)
Verificatore in banda per GHSA-c4j6-fc7j-m34r / CVE-2026-44578 — Server-Side Request Forgery in Next.js tramite richieste di upgrade WebSocket.
⚠️ Solo per test di sicurezza autorizzati. Sei responsabile di assicurarti di avere il permesso di testare ogni target che passi a questo script.
| Campo | Valore |
|---|---|
| CVE | CVE-2026-44578 |
| GHSA | GHSA-c4j6-fc7j-m34r |
| CWE | CWE-918 (SSRF) |
| CVSS v3.1 | 8.6 (Alta) — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N |
| Affette | next >=13.4.13 <15.5.16, >=16.0.0 <16.2.5 |
| Corrette | 15.5.16, 16.2.5 |
| Commit fix | c4f69086 |
| Non affette | Ospitate su Vercel; output: "export"; deployment dietro un reverse proxy che non inoltra Upgrade |
Un attaccante apre una connessione TCP a un processo Next.js self-hosted e invia una richiesta di upgrade WebSocket HTTP/1.1 il cui request-URI è un URL assoluto:
GET http://anything/<path> HTTP/1.1
Host: <target>
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
In resolveRoutes, l'URL contiene // (ogni URI assoluto lo fa), che
corrisponde al ramo "normalizza slash ripetuti". Quel ramo restituisce subito
con { finished: true, statusCode: 308, parsedUrl: <mangled> }. Il
normalizzatore comprime http://host/path in http:/host/path (uno slash).
In router-server.ts, il gestore di upgrade pre-patch ignorava
finished/statusCode e controllava solo parsedUrl.protocol. Poiché il
protocollo sopravvive alla normalizzazione, chiamava proxyRequest(...).
proxyRequest esegue url.format(parsedUrl) sull'URL distorto, ottenendo
http:/host:port/path. http-proxy analizza quel target, non trova host
(url.parse('http:/...').host === null) e ricade sul suo default:
localhost:80 (o localhost:443 per https).
Quindi in pratica l'SSRF consente di far aprire a Next un upgrade WebSocket verso localhost:80 / localhost:443 del proprio host con un percorso controllato dall'attaccante.
La correzione (commit c4f69086) ha fatto sì che il gestore di upgrade
controlli finished && !statusCode prima di inoltrare. Il caso di
normalizzazione con 308 ora fallisce il controllo !statusCode e il socket
viene chiuso.
Il proxy non raggiunge mai un host esterno. Se configuri un interactsh /
Burp Collaborator / webhook canary e ti aspetti che il processo Next chiami
casa, non lo farà — la connessione va a localhost sulla macchina target.
Questo verificatore usa quindi un segnale in banda letto dal socket di
upgrade: un server vulnerabile restituisce un body di errore riconoscibile,
un server corretto non restituisce nulla.
Il target SSRF è limitato ma comunque significativo in deployment reali:
127.0.0.1:80 o :443 e si fidano di richieste
originate da localhost.127.0.0.1:80 (raro ma visto).Gli endpoint metadata di AWS / GCP / Azure (169.254.169.254) non sono
direttamente raggiungibili perché il bug fissa la destinazione su localhost.
Per ogni target, lo script apre un socket TCP (o TLS) grezzo, invia l'upgrade artefatto, legge la risposta e produce due segnali:
verdict — se il bug è presente.impact_confirmed — se l'SSRF ha effettivamente esfiltrato dati
(cioè un servizio co-locato su localhost:80/443 del target ha risposto
e ne abbiamo ricevuto la risposta).| Risposta | Verdetto | impact_confirmed |
|---|---|---|
Contiene Internal Server Error | vulnerable | false — bug provato, ma il proxy non ha colpito nulla su localhost |
Inizia con HTTP/1. | vulnerable_proxy_succeeded | true — esfiltrati dati reali della risposta |
| Vuoto / chiusura pulita | likely_patched | false — copre anche "non Next", "reverse proxy ha rimosso Upgrade", "Vercel" |
| Identico al controllo senza Upgrade | front_end_intercepts | false — proxy front-end ha short-circuito entrambi i probe; SSRF non ha mai raggiunto Next |
| Qualsiasi altra cosa | inconclusive | false |
Quando impact_confirmed è true, l'output JSON include anche
upstream_status, upstream_server e upstream_content_type
estratti dalla risposta trapelata (utile per triage / scrittura report).
Per impostazione predefinita, ogni target riceve anche un probe di controllo
con la stessa linea di richiesta URI assoluto ma senza header Upgrade
(Connection: close). Se il front-end restituisce la stessa risposta a
entrambi i probe (linea di stato + dimensione entro tolleranza), significa che
il front-end dell'host sta rifiutando la linea di richiesta URI assoluto di per
sé — nginx 400, Apache 400, CDN edge — e l'SSRF non ha mai raggiunto Next.
Il verdetto viene degradato a front_end_intercepts e l'output JSON include
front_end_status e front_end_server estratti dalla risposta del proxy
in modo che l'operatore possa identificare cosa sta intercettando.
Questo elimina un falso positivo reale osservato quando Next self-hosted
si trova dietro nginx/Apache: quei proxy rifiutano la linea di richiesta
GET http:///x HTTP/1.1 del probe con un 400 generico, che il rilevatore
in precedenza interpretava erroneamente come vulnerable_proxy_succeeded.
Usa --no-control-probe per rinunciare e vedere i verdetti grezzi.
# singolo target
python3 verify_ghsa_c4j6.py --target https://app.example.com
# più target ripetendo il flag
python3 verify_ghsa_c4j6.py \
--target https://app1.example.com \
--target app2.example.com:3000 \
--target 10.0.0.5:80
# da file (un target per riga; '#' per commenti)
python3 verify_ghsa_c4j6.py --targets-file targets.txt
# da stdin
cat targets.txt | python3 verify_ghsa_c4j6.py
# output in JSON Lines per tool downstream
python3 verify_ghsa_c4j6.py --targets-file targets.txt --json
# Enumera servizi co-locati su localhost:80/443 del target tramite il bug
python3 verify_ghsa_c4j6.py --target https://app.example.com --scan
# Come sopra, con una lista di percorsi personalizzata
python3 verify_ghsa_c4j6.py --target ... --scan-paths-file my_paths.txt
--scan sonda una lista incorporata di percorsi comuni (moduli status di
Apache/nginx, endpoint health & metrics, Spring Boot Actuator, Go pprof,
endpoint del daemon Docker, pannelli admin comuni, file di configurazione
esposti, route di Elasticsearch, ecc.) attraverso il gadget SSRF.
Per impostazione predefinita, la modalità scan esegue un probe di baseline
differenziale aggiuntivo con un percorso inesistente casuale per target.
I probe successivi vengono etichettati DIFF solo quando la loro firma
(status, lunghezza body) diverge dalla baseline — gli 404 uniformi da un
upstream "non ha trovato nulla" vengono marcati come noise e non gonfiano
il conteggio dei hit. Usa --no-differential per registrare ogni probe che
ha raggiunto un servizio (comportamento legacy).
L'output è raggruppato per target: