
Фильтр SSRF проверял текст имени хоста, но фактическое назначение определялось позже через DNS. Этот разрыв позволял управляемым злоумышленником URL-адресам Webhook достигать loopback, метаданных и целей частной сети.
Фильтр SSRF проверял текст имени хоста, но фактический адрес назначения определялся позже через DNS. Этот разрыв позволил URL-адресам вебхуков, контролируемым атакующим, достигать loopback, метаданных и частных сетевых целей.
Я обнаружил эту проблему во время проверки Typebot, открытого конструктора чат-ботов, задав простой вопрос безопасности:
Что происходит, если защита от SSRF проверяет текст имени хоста, а реальный адрес назначения определяется позже через DNS?
В данном случае этот вопрос привёл к реальной ошибке.
Защита от SSRF в Typebot для блоков Webhook / HTTP Request проверяла только:
Она не выполняла разрешение DNS-имён перед разрешением запроса.
Это означало, что имя хоста, например ssrf-repro.example, могло выглядеть безвредно при проверке, а затем разрешаться в:
127.0.0.1169.254.169.254и всё равно быть загруженным HTTP-клиентом на серверной стороне.
Эта проблема стала CVE-2026-34207.
Typebot: Typebot на GitHub
CVE: CVE-2026-34207
Исправлено в: 3.16.0
Это затронуло Typebot, широко используемую платформу для создания чат-ботов с открытым исходным кодом. На официальном сайте Typebot представлен как продукт, которому доверяют более 650 компаний по всему миру. На сайте также рекламируются 2 млн+ чатов в месяц и 1,5 млн+ опубликованных ботов.
URL вебхука, контролируемый атакующим -> имя хоста проходит проверку SSRF только на литералы -> принятие решения без разрешения DNS -> HTTP-клиент на серверной стороне разрешает имя хоста во внутреннюю цель -> серверный запрос достигает loopback / метаданных / частной сети -> данные ответа становятся доступны через журналы выполнения
Typebot — это конструктор чат-ботов.
Он позволяет пользователям создавать сценарии, которые могут:
Webhook / HTTP RequestТаким образом, выполнение исходящих запросов — это реальная граница безопасности.
Важный вопрос здесь заключался не в том, поддерживает ли Typebot блоки Webhook.
Настоящий вопрос был таков:
Проверяет ли защита SSRF фактический адрес назначения, к которому подключится сервер, или только текст имени хоста, указанный в URL?
В данном случае сначала проверялась только текстовая форма.
Это и было ошибкой.
Защиты от SSRF терпят неудачу предсказуемым образом.
Чаще всего интересные ошибки — это не:
169.254.169.254»localhost»Более значимые ошибки — это ошибки границ:
Вот где стоило искать.
У Typebot уже была логика защиты от SSRF для литеральных IP-адресов метаданных, loopback, частных диапазонов и трюков с закодированными IP.
Это сделало следующий вопрос очевидным:
Что, если имя хоста не является буквально опасным, но позже разрешается в опасный адрес?
Именно это и произошло.
Корень проблемы — проверка адреса назначения по тексту имени хоста, а не по разрешённому IP-адресу.
В уязвимой реализации validateHttpReqUrl():
http: и https:metadata.google.internal, metadata.goog, metadata и localhostЭто важная часть.
Если имя хоста было обычным значением, например:
ssrf-repro.example
то parseIPAddress(hostname) возвращал null, и проверка на этом останавливалась.
Перед одобрением не происходило разрешения DNS.
Таким образом, уязвимая логика фактически сводилась к:
const ip = parseIPAddress(hostname);
if (ip) {
validateIPAddress(ip);
}
Это означает:
Вторая половина ошибки находилась в пути выполнения.
В executeHttpRequest() Typebot сначала запускал проверку, а затем выполнял фактический запрос через ky(request.url, ...).
Таким образом, последовательность была такова:
Это и есть вся уязвимость.
Важное различие — где принималось решение о доверии.
Многие ошибки выглядят мелкими, если их плохо описать.
Если описать эту ошибку как:
«фильтр имён хостов был неполным»
это звучит как проблема качества.
Но настоящая проблема заключалась в другом:
Это не косметический недостаток фильтрации.
Это нарушение границы доверия.
И поскольку HTTP-исполнитель записывал данные ответа в журналы выполнения, проблема не была даже «слепой» в самых строгих случаях.
Так что это было не просто:
Это было:
Это настоящая уязвимость SSRF.
Я использовал два уровня доказательств, потому что они демонстрируют разные вещи.
Первый PoC чисто выделил первопричину.
Я использовал небольшой локальный стенд, который:
127.0.0.1http://ssrf-repro.example:18080/...ssrf-repro.example разрешалось в 127.0.0.1Это продемонстрировало точную ошибку:
Зафиксированный вывод показал:
Это напрямую доказывало пробел в проверке.
Второй PoC показал ошибку через фактический путь функции, который имеет значение.
Самое простое воспроизведение:
Webhook, указывающий на это безобидное имя хостаПримерный блок выглядел так:
{
"id": "blk-webhook",
"type": "Webhook",
"options": {
"webhook": {
"method": "GET",
"url": "http://ssrf-repro.example:8000/"
}
}
}
С записью в файле hosts, например:
127.0.0.1 ssrf-repro.example
проверка всё равно принимала URL, потому что:
ssrf-repro.example не было в blockedHostnameslocalhostparseIPAddress("ssrf-repro.example") возвращал null