
O filtro SSRF verificava o texto do hostname, mas o destino real era decidido posteriormente pelo DNS. Essa lacuna permitia que URLs de Webhook controladas pelo atacante alcançassem alvos de loopback, metadados e rede privada.
O filtro SSRF verificava o texto do hostname, mas o destino real era decidido mais tarde pelo DNS. Essa lacuna permitia que URLs de Webhook controladas pelo atacante alcançassem loopback, metadados e alvos de rede privada.
Encontrei esse problema ao revisar Typebot, um construtor de chatbots open-source, com uma simples questão de segurança em mente:
O que acontece se a proteção SSRF valida o texto do hostname, mas o destino real é decidido mais tarde pelo DNS?
Neste caso, essa questão levou a um bug real.
A proteção SSRF do Typebot para blocos Webhook / HTTP Request validava apenas:
Ela não resolvia hostnames antes de permitir a requisição.
Isso significava que um hostname como ssrf-repro.example poderia parecer inofensivo durante a validação, mas depois resolver para:
127.0.0.1169.254.169.254e ainda ser obtido pelo cliente HTTP do backend.
Esse problema se tornou CVE-2026-34207.
Typebot: Typebot no GitHub
CVE: CVE-2026-34207
Corrigido em: 3.16.0
Isso afetou Typebot, uma plataforma de chatbot open-source amplamente utilizada. Em seu site oficial, o Typebot é apresentado como confiável por 650+ empresas mundialmente. O site também anuncia 2M+ chats mensais e 1.5M+ bots publicados.
attacker-controlled Webhook URL -> hostname passes literal-only SSRF validation -> no DNS resolution before allow decision -> backend HTTP client resolves hostname to internal target -> server-side request reaches loopback / metadata / private network -> response data becomes available through execution logs
Typebot é um construtor de chatbots.
Ele permite que usuários criem fluxos que podem:
Webhook / HTTP RequestIsso significa que a execução de requisições de saída é uma fronteira de segurança real.
A questão importante aqui não era se o Typebot suportava blocos Webhook.
A verdadeira questão era:
A proteção SSRF valida o destino real ao qual o servidor se conectará, ou apenas o texto do hostname que aparece na URL?
Neste caso, ela validou apenas a forma textual primeiro.
Esse foi o erro.
Defesas SSRF falham de maneiras muito previsíveis.
Na maioria das vezes, os erros interessantes não são:
169.254.169.254"localhost"Os erros mais fortes são erros de fronteira:
Esse era o lugar certo para olhar aqui.
O Typebot já possuía lógica de hardening SSRF para IPs de metadados literais, loopback, faixas privadas e truques de IP codificados.
Isso tornou a próxima pergunta óbvia:
E se o hostname não é literalmente perigoso, mas resolve para um destino perigoso mais tarde?
Foi exatamente o que aconteceu.
A raiz do problema foi validação de destino baseada no texto do hostname em vez do endereço IP resolvido.
Na implementação vulnerável, validateHttpReqUrl():
http: e https:metadata.google.internal, metadata.goog, metadata e localhostEssa é a parte importante.
Se o hostname fosse um valor normal como:
ssrf-repro.example
então parseIPAddress(hostname) retornava null, e o validador parava por aí.
Nenhuma resolução DNS ocorria antes da aprovação.
Então a lógica vulnerável efetivamente se resumia a:
const ip = parseIPAddress(hostname);
if (ip) {
validateIPAddress(ip);
}
Isso significa que:
A segunda metade do bug estava no caminho de execução.
Em executeHttpRequest(), o Typebot primeiro executava a validação e depois realizava a requisição real com ky(request.url, ...).
Então a sequência era:
Essa é a vulnerabilidade completa.
A distinção importante é onde a decisão de confiança foi tomada.
Muitos bugs parecem pequenos se descritos mal.
Se descrevermos este como:
"o filtro de hostname estava incompleto"
parece um problema de qualidade.
Esse não é o verdadeiro problema.
O verdadeiro problema era:
Isso não é uma fraqueza cosmética de filtragem.
Isso é uma falha na fronteira de confiança.
E porque o executor HTTP gravava dados de resposta nos logs de execução, o problema nem era cego nos casos mais fortes.
Então não era apenas:
Era:
Isso é uma verdadeira vulnerabilidade SSRF.
Usei duas camadas de prova porque demonstravam duas coisas diferentes.
A primeira PoC isolou a causa raiz de forma limpa.
Usei uma pequena estrutura local que:
127.0.0.1http://ssrf-repro.example:18080/...ssrf-repro.example resolvesse para 127.0.0.1Isso demonstrou a falha exata:
A saída capturada mostrava:
Isso comprovou a lacuna de validação diretamente.
A segunda PoC mostrou o bug através do caminho real do recurso que importa.
A reprodução mais simples era:
Webhook apontando para esse hostname benignoUm bloco representativo se parecia com:
{
"id": "blk-webhook",
"type": "Webhook",
"options": {
"webhook": {
"method": "GET",
"url": "http://ssrf-repro.example:8000/"
}
}
}
Com uma entrada no arquivo hosts como:
127.0.0.1 ssrf-repro.example
o validador ainda aceitava a URL porque:
ssrf-repro.example não estava em blockedHostnameslocalhostparseIPAddress("ssrf-repro.example") retornou null