
Verificador OOB para GHSA-c4j6-fc7j-m34r / CVE-2026-44578 (Next.js WebSocket-upgrade SSRF)
Verificador em banda para GHSA-c4j6-fc7j-m34r / CVE-2026-44578 — Server-Side Request Forgery no Next.js via requisições de upgrade WebSocket.
⚠️ Apenas para testes de segurança autorizados. Você é responsável por garantir que tem permissão para testar cada alvo que passar para este script.
| Campo | Valor |
|---|---|
| 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 |
| Afetado | next >=13.4.13 <15.5.16, >=16.0.0 <16.2.5 |
| Corrigido | 15.5.16, 16.2.5 |
| Commit de correção | c4f69086 |
| Não afetado | Hospedado na Vercel; output: "export"; implantações atrás de um proxy reverso que não encaminha Upgrade |
Um atacante abre uma conexão TCP para um processo Next.js auto-hospedado e envia um upgrade WebSocket HTTP/1.1 cujo request-URI é uma URL absoluta:
GET http://anything/<path> HTTP/1.1
Host: <target>
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Em resolveRoutes, a URL contém // (como toda URI absoluta), o que corresponde ao ramo "normalizar barras repetidas". Esse ramo retorna cedo com { finished: true, statusCode: 308, parsedUrl: <mangled> }. O normalizador colapsa http://host/path em http:/host/path (uma barra).
Em router-server.ts, o manipulador de upgrade pré-correção ignorou finished/statusCode e apenas verificou parsedUrl.protocol. Como o protocolo sobrevive à normalização, ele chamou proxyRequest(...).
proxyRequest executa url.format(parsedUrl) na URL corrompida, obtendo http:/host:port/path. http-proxy analisa esse destino, não encontra host (url.parse('http:/...').host === null), e retorna ao seu destino padrão: localhost:80 (ou localhost:443 para https).
Então, na prática, o SSRF permite que você faça o Next abrir um upgrade WebSocket para o próprio localhost:80 / localhost:443 do host Next.js com um caminho controlado pelo atacante.
A correção (commit c4f69086) fez com que o manipulador de upgrade verificasse finished && !statusCode antes de fazer o proxying. O caso de normalização 308 agora falha na verificação !statusCode e o socket é fechado em vez disso.
O proxy nunca alcança um host externo. Se você configurar um interactsh / Burp Collaborator / webhook canário e esperar que o processo Next telefone para casa, não vai — a conexão vai para localhost na máquina alvo. Este verificador, portanto, usa um sinal em banda lido do socket de upgrade: um servidor vulnerável retorna um corpo de erro reconhecível, um servidor corrigido não retorna nada.
O alvo do SSRF é restrito, mas ainda significativo em implantações reais:
127.0.0.1:80 ou :443 e confiam em requisições originadas de localhost.127.0.0.1:80 (incomum, mas visto).Os endpoints de metadados da AWS / GCP / Azure (169.254.169.254) não são diretamente acessíveis porque o bug fixa o destino em localhost.
Para cada alvo, o script abre um socket TCP bruto (ou TLS), envia o upgrade criado, lê a resposta e produz dois sinais:
verdict — se o bug está presente.impact_confirmed — se o SSRF realmente exfiltrou dados (ou seja, um serviço co-localizado em localhost:80/443 do alvo respondeu e recebemos sua resposta de volta).| Resposta | Verdict | impact_confirmed |
|---|---|---|
Contém Internal Server Error | vulnerable | false — bug comprovado, mas o proxy não encontrou nada em localhost |
Começa com HTTP/1. | vulnerable_proxy_succeeded | true — dados reais da resposta exfiltrados |
| Vazio / fechamento limpo | likely_patched | false — também cobre "não é Next", "proxy reverso removeu Upgrade", "Vercel" |
| Idêntico ao controle sem Upgrade | front_end_intercepts | false — proxy front-end interrompeu ambas as sondas; SSRF nunca alcançou o Next |
| Qualquer outra coisa | inconclusive | false |
Quando impact_confirmed é true, a saída JSON também inclui upstream_status, upstream_server e upstream_content_type analisados da resposta vazada (útil para triagem/elaboração de relatórios).
Por padrão, cada alvo também recebe uma sonda de controle com a mesma linha de requisição de URI absoluta, mas sem cabeçalhos Upgrade (Connection: close). Se o front-end retornar a mesma resposta para ambas as sondas (linha de status + tamanho dentro da tolerância), o front-end do próprio host está rejeitando a linha de requisição de URI absoluta em si — nginx 400, Apache 400, CDN edge — e o SSRF nunca alcançou o Next. O veredito é rebaixado para front_end_intercepts e a saída JSON inclui front_end_status e front_end_server analisados da resposta do proxy para que o operador possa identificar o que está interceptando.
Isso elimina um falso positivo real observado quando Next auto-hospedado está atrás de nginx/Apache: esses proxies rejeitam a linha de requisição GET http:///x HTTP/1.1 da sonda com um 400 genérico, que o detector anteriormente interpretava erroneamente como vulnerable_proxy_succeeded. Use --no-control-probe para optar por não participar e ver os vereditos brutos.
# alvo único
python3 verify_ghsa_c4j6.py --target https://app.example.com
# múltiplos alvos através de repetição de bandeira
python3 verify_ghsa_c4j6.py \
--target https://app1.example.com \
--target app2.example.com:3000 \
--target 10.0.0.5:80
# a partir de um arquivo (um alvo por linha; '#' para comentários)
python3 verify_ghsa_c4j6.py --targets-file targets.txt
# a partir de stdin
cat targets.txt | python3 verify_ghsa_c4j6.py
# saída em JSON Lines para ferramentas downstream
python3 verify_ghsa_c4j6.py --targets-file targets.txt --json
# Enumerar serviços co-localizados no localhost:80/443 do alvo através do bug
python3 verify_ghsa_c4j6.py --target https://app.example.com --scan
# O mesmo, com uma lista de caminhos personalizada
python3 verify_ghsa_c4j6.py --target ... --scan-paths-file my_paths.txt
--scan sonda uma lista embutida de caminhos comuns (módulos de status Apache/nginx, endpoints de health & métricas, Spring Boot Actuator, Go pprof, endpoints do Docker daemon, painéis administrativos comuns, arquivos de configuração vazados, rotas do Elasticsearch, etc.) através do gadget SSRF.
Por padrão, o modo de varredura executa uma sonda de linha de base diferencial extra com um caminho aleatório inexistente por alvo. As sondas subsequentes são marcadas como DIFF apenas quando sua assinatura (status, comprimento do corpo) diverge da linha de base — 404s uniformes de um upstream que "não encontrou nada" são marcados como noise e não inflam a contagem de acertos. Use --no-differential para relatar todas as sondas que alcançaram um serviço (comportamento legado).
A saída é agrupada por alvo: