
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: CVE-2026-34207
3.16.0Isso 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 nullEntão o cliente HTTP do backend resolveu o hostname para 127.0.0.1 e conectou de qualquer forma.
Isso estabeleceu a alegação completa:
A primeira PoC prova a causa raiz.
A segunda PoC prova o impacto no produto.
Essa divisão é importante.
Se você mostra apenas:
"este filtro aceita um hostname"
você não mostrou o suficiente.
Se você mostra apenas:
"uma requisição interna aconteceu"
você não isolou o porquê.
O relatório mais forte é:
Essa é a história completa.
Este problema foi razoavelmente classificado como Alta.
A classificação do aviso foi:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L
Isso faz sentido.
Isso não era um SSRF não autenticado em toda a internet por si só.
Os privilégios exigidos eram Baixos porque o bug independente exigia um ator que pudesse configurar ou acionar um bloco Webhook / HTTP Request.
Mas dentro desse limite, o impacto era sério:
Isso é um problema SSRF forte por si só.
E fica ainda mais perigoso quando combinado com outros bugs que ampliam a alcançabilidade.
Algumas pessoas veem SSRF autenticado e imediatamente o subestimam.
Isso é um erro.
A verdadeira questão não é:
"O atacante estava logado?"
A verdadeira questão é:
"Esse atacante poderia fazer o servidor se conectar a algum lugar que o modelo de segurança deveria proibir?"
Aqui, a resposta foi sim.
O validador afirmava proteger:
Mas uma lacuna de resolução de hostname permitiu que esses mesmos destinos voltassem através de uma representação diferente.
Esse é exatamente o tipo de bug que merece um CVE.
A correção foi sólida porque corrigiu a fronteira real, não apenas um sintoma.
Na implementação corrigida, validateHttpReqUrl() foi alterada para resolver hostnames antes de aprovar a requisição.
O validador agora:
lookup de node:dns/promisesEssa é a correção certa porque muda a decisão de confiança de:
para:
Essa é a propriedade de segurança que deveria ter existido desde o início.
A remediação também adicionou cobertura de regressão para:
Esse é o tipo de hardening que você quer em uma correção SSRF real:
O problema foi resolvido no Typebot 3.16.0.
Este problema foi reportado de forma privada através do fluxo de relatórios de segurança do GitHub.
O relatório incluía:
validateHttpReqUrl()executeHttpRequest()Os mantenedores aceitaram o problema, corrigiram a lógica de validação, e a vulnerabilidade foi posteriormente publicada como:
CVE-2026-34207
com a correção lançada no Typebot 3.16.0.
A lição chave aqui é simples:
decisões de segurança devem ser tomadas na mesma representação que a pilha de rede realmente usará.
Isso parece óbvio.
Mas muitas proteções SSRF falham exatamente porque não seguem essa regra.
Isso é suficiente.
Este bug também reforça algo importante sobre revisão SSRF em geral:
Se você não resolver e validar o destino real, seu filtro SSRF ainda está incompleto.
Essa é a verdadeira lição.
Esta vulnerabilidade não era sobre um payload sofisticado.
Era sobre fazer a pergunta certa sobre a fronteira.
No Typebot, o validador SSRF verificava o texto do hostname primeiro. O destino real era decidido mais tarde pelo DNS. O cliente de rede seguia o endereço resolvido.
Essa lacuna era o bug.
É por isso que isso se tornou CVE-2026-34207.
Corrigido no Typebot 3.16.0.