
Фильтр 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.
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Затем HTTP-клиент на серверной стороне разрешал имя хоста в 127.0.0.1 и всё равно подключался.
Это подтверждало полное утверждение:
Первый PoC доказывает первопричину.
Второй PoC доказывает влияние на продукт.
Это разделение важно.
Если показать только:
«этот фильтр принимает имя хоста»
этого недостаточно.
Если показать только:
«произошёл внутренний запрос»
вы не изолировали причину.
Более сильный отчёт таков:
Это полная история.
Эта проблема была обоснованно классифицирована как Высокая.
Классификация в уведомлении:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L
Это имеет смысл.
Это не был неаутентифицированный общедоступный SSRF сам по себе.
Требуемые привилегии были Низкими, потому что для отдельной ошибки требовался субъект, который мог настроить или запустить блок Webhook / HTTP Request.
Но в этих границах влияние было серьёзным:
Это сильная проблема SSRF сама по себе.
И она становится ещё опаснее в сочетании с другими ошибками, расширяющими охват.
Некоторые люди видят аутентифицированный SSRF и сразу занижают его оценку.
Это ошибка.
Настоящий вопрос не в том:
«Был ли атакующий авторизован?»
Настоящий вопрос в том:
«Мог ли этот атакующий заставить сервер подключиться туда, куда модель безопасности должна была запрещать?»
Здесь ответ — да.
Проверка утверждала, что защищает:
Но пробел в разрешении имён хостов позволил тем же адресам назначения вернуться через другое представление.
Это именно та ошибка, которая заслуживает CVE.
Исправление было надёжным, потому что оно исправило реальную границу, а не просто симптом.
В исправленной реализации validateHttpReqUrl() была изменена на разрешение DNS-имён перед одобрением запроса.
Теперь проверка:
lookup из node:dns/promisesЭто правильное исправление, потому что оно меняет решение о доверии с:
на:
Это свойство безопасности, которое должно было существовать с самого начала.
Исправление также добавило регрессионные тесты для:
Это тот вид усиления, который вы хотите видеть в реальном исправлении SSRF:
Проблема была исправлена в Typebot 3.16.0.
Эта проблема была сообщена приватно через поток безопасности на GitHub.
Отчёт включал:
validateHttpReqUrl()executeHttpRequest()Сопровождающие приняли проблему, исправили логику проверки, и уязвимость позже была опубликована как:
CVE-2026-34207
с исправлением, выпущенным в Typebot 3.16.0.
Ключевой урок прост:
решения безопасности должны приниматься на основе того же представления, которое будет использовать сетевой стек.
Это звучит очевидно.
Но многие защиты SSRF терпят неудачу именно потому, что не следуют этому правилу.
Этого достаточно.
Эта ошибка также подкрепляет важный момент при анализе SSRF в целом:
Если вы не разрешаете и не проверяете реальный адрес назначения, ваш фильтр SSRF всё ещё неполон.
Вот настоящий вывод.
Эта уязвимость не была связана с хитрой полезной нагрузкой.
Она заключалась в постановке правильного вопроса о границах.
В Typebot проверка SSRF сначала проверяла текст имени хоста. Фактический адрес назначения определялся позже через DNS. Сетевой клиент следовал разрешённому адресу.
Этот разрыв и был ошибкой.
Вот почему это стало CVE-2026-34207.
Исправлено в Typebot 3.16.0.