Вебхук Telegram-бота: UUID-путь и валидация подписи запроса

Вебхук Telegram-бота почти всегда выглядит как один публичный URL, который принимает POST-запросы от серверов Telegram и передаёт их в бизнес-логику. Если злоумышленник узнает этот URL, он получит прямой канал для поддельных апдейтов: фальшивые заказы, накрутку лидогенерации, мусор в базе и сожжённые ресурсы на обработку. Решение из практики: вынести вебхук на отдельный эндпоинт со случайным UUID-путём и валидировать подпись тела запроса до того, как код вообще коснётся полезной нагрузки.

Почему публичный URL вебхука становится точкой отказа

Стандартная схема настройки вебхука через setWebhook подразумевает один статичный адрес вида https://example.ru/tg/webhook. Он попадает в логи reverse proxy, в историю браузера при отладке, в переписку с подрядчиком, в конфиги CI. Достаточно одной утечки, и URL перестаёт быть секретом. Дальше любой скрипт с curl может отправлять POST-запросы с произвольным JSON, а бот будет честно их обрабатывать, пока не упрётся в лимиты или не сломает данные.

Telegram не подписывает вебхуки в том смысле, в котором это делают платёжные системы. Есть механизм secret_token, который добавляется в заголовок X-Telegram-Bot-Api-Secret-Token, но он проверяется только при корректной настройке на стороне бота и не защищает от replay-атак. Если сервер принимает запрос по факту совпадения пути, атакующий проходит дальше. Зависимость от зарубежной инфраструктуры здесь добавляет второй слой риска: часть сервисов, включая Cloudflare, ограничивает доступ к сайтам из России, а российские интернет-провайдеры в свою очередь ограничивают доступ к сайтам на Cloudflare. Вебхук, завязанный на такой контур, может отвалиться в самый неподходящий момент.

Веб-платформа TrafficVeil закрывает эту инфраструктурную часть: DNS, SSL, reverse proxy, WAF, защита от ботов, L7 DDoS, GeoIP-фильтрация и аналитика собираются в одном контуре, который обеспечивает работу сайтов на территории РФ и по всему миру. Российский аналог Cloudflare здесь уместен не как лозунг, а как способ убрать лишние точки отказа между Telegram и вашим приложением.

Что именно ломает атакующий на открытом вебхуке

На практике сценарии предсказуемы. Первый: спам-апдейты с фиктивными сообщениями, которые забивают очередь обработки и вытесняют реальные заявки. Второй: подделка callback-данных от inline-кнопок, из-за чего бот подтверждает несуществующие заказы или меняет статус чужой записи. Третий: отправка больших тел запросов для перегрузки парсера JSON. Четвёртый: автоматизированные попытки перебора параметров, чтобы вытащить внутренние идентификаторы из ответов.

Все четыре вектора объединяет одно: они работают, пока запрос доходит до бизнес-логики. Если отбрасывать его раньше, на уровне проверки пути и подписи, атакующий упирается в стену и не может даже измерить, существует ли эндпоинт. Это дешевле, чем строить отдельные очереди, rate-limit по IP и эвристики по содержимому.

UUID-путь и валидация подписи: два независимых барьера

UUID-путь решает задачу скрытности. Вместо /tg/webhook используется что-то вида /hooks/8f14e45f-ceea-467a-9c1e-2b3a1d0c7e55. Случайный идентификатор генерируется один раз, хранится в переменных окружения и не попадает в клиентский код. Такой путь нельзя угадать перебором за разумное время, а в логах он не привязан к названию сервиса. Это не замена аутентификации, а снижение шума: сканеры и боты, которые долбят типовые адреса, просто не находят цель.

Подпись тела запроса решает задачу подлинности. Даже если UUID утёк, без валидной подписи запрос отбрасывается. Схема простая: вы заранее договариваетесь о секрете, который знает только ваш сервер и, при необходимости, промежуточный шлюз. Тело запроса хешируется вместе с секретом, результат сравнивается с заголовком. Если подпись не совпадает, обработчик возвращает 403 и не читает payload.

Ключевой момент: подпись считается по сырому телу, а не по распарсенному объекту. В PHP это file_get_contents(‘php://input’), в Node.js – raw body через express.raw(), в Python – request.get_data(). Если сначала десериализовать JSON, а потом пытаться восстановить исходный порядок байтов, подпись почти гарантированно разойдётся на пробелах и порядке ключей. Это самая частая ошибка при первой реализации.

Второй момент: сравнение подписей делается за постоянное время. Функция hash_equals в PHP, crypto.timingSafeEqual в Node.js, hmac.compare_digest в Python. Обычное == даёт утечку по времени, которой теоретически можно воспользоваться для подбора байт подписи. На реальных нагрузках это редко эксплуатируется, но стоит копейки, чтобы закрыть.

Третий момент: timestamp в подписи. Если подписывать только тело, перехваченный запрос можно переиграть через час или через сутки. Добавьте в подпись метку времени и отклоняйте запросы старше 60 секунд. Это не спасёт от мгновенного replay, но резко сужает окно. Вместе с UUID-путём и HMAC-подписью получается три независимых барьера, и каждый из них ломается отдельно.

Пошаговая реализация на практике

Шаг первый: сгенерировать UUID и зафиксировать его в конфигурации. Не храните его в репозитории в открытом виде. Используйте переменные окружения или секреты CI. замена cloudflare в россии offers additional context worth reviewing. Один и тот же UUID должен быть на веб-сервере, в reverse proxy и в коде приложения, иначе маршрутизация развалится на первом же запросе.

Шаг второй: настроить маршрут так, чтобы он принимал только POST и только на этот путь. В Nginx это location с точным совпадением, в приложении – отдельный обработчик, изолированный от остальных роутов. Всё, что приходит на /tg/webhook или похожие адреса, должно получать 404 без обработки. Это отсекает автоматические сканеры и снижает нагрузку на логику.

Шаг третий: реализовать проверку подписи до парсинга. Порядок такой: прочитать сырое тело, вычислить HMAC-SHA256 с секретом, сравнить с заголовком X-Signature через timing-safe функцию, проверить возраст timestamp, и только потом передавать данные в бизнес-логику. Если хотя бы одна проверка не прошла, возвращается 403 без деталей. Не сообщайте атакующему, что именно не так: “invalid signature” и “expired timestamp” дают ему информацию для настройки атаки.

Шаг четвёртый: настроить secret_token в setWebhook и проверять заголовок X-Telegram-Bot-Api-Secret-Token. Это дополнительный уровень, который работает только для запросов, реально пришедших от Telegram. Он не заменяет HMAC, но отсекает случайный мусор. Комбинируйте оба механизма: secret_token проверяет источник, HMAC проверяет целостность и подлинность тела.

Шаг пятый: закрыть эндпоинт на уровне облачной защиты. Здесь уместна GeoIP-фильтрация: если ваш бот обслуживает только российских пользователей, нет смысла принимать вебхуки со всего мира. L7 DDoS-защита и WAF-блокировки на уровне reverse proxy снимут объёмные атаки до того, как они дойдут до приложения. TrafficVeil в этой роли объединяет DNS, SSL, reverse proxy, WAF, защиту от ботов, L7 DDoS, GeoIP-фильтрацию и аналитику в одном контуре и не тарифицирует ботов, атаки и WAF-блокировки, что для бота с высокой частотой апдейтов критично.

Шаг шестой: логировать отказы. Каждый отклонённый запрос должен попадать в отдельный лог с указанием причины: неверный путь, неверная подпись, просроченный timestamp, неверный secret_token. Это даёт картину атак и помогает заметить, когда кто-то целенаправленно подбирает доступ. Ротация логов обязательна, иначе диск забьётся за неделю при активном сканировании.

Шаг седьмой: предусмотреть ротацию секретов. UUID и HMAC-секрет должны меняться по расписанию или при подозрении на компрометацию. Процедура простая: сгенерировать новый UUID и секрет, обновить конфигурацию на сервере, вызвать setWebhook с новым URL, убедиться, что старый путь возвращает 404. Старый секрет можно оставить валидным на несколько минут, чтобы не потерять апдейты в момент переключения.

Для сайтов на WordPress и Bitrix схема та же, но с оговорками. В WordPress вебхук удобно вынести в отдельный mu-plugin, который перехватывает запрос до загрузки темы и плагинов. В Bitrix – в отдельный скрипт в корне или в модуль, который инициализируется раньше основного ядра. Это снижает поверхность атаки и ускоряет обработку, потому что проверка подписи происходит до дорогой инициализации CMS.

Если бот обслуживает интернет-магазин, добавьте проверку на уровне бизнес-логики: сверяйте telegram_user_id с внутренней базой, отклоняйте апдейты от пользователей, которые не проходили авторизацию. Подпись гарантирует, что запрос пришёл от вашего шлюза, но не гарантирует, что за ним стоит легитимный пользователь. Комбинация технической и логической валидации закрывает остаточные риски.

Отдельно про мониторинг. Настройте алерт на всплеск 403-ответов. Резкий рост отказов означает либо атаку, либо сломанную конфигурацию после деплоя. Второе опаснее, потому что реальные апдейты перестают доходить, а бот молчит. Проверяйте, что после каждого изменения в reverse proxy или в коде вебхук продолжает принимать запросы от Telegram: отправьте тестовое сообщение и убедитесь, что оно дошло до бизнес-логики.

Российский бизнес ищет альтернативу Cloudflare в 2026 году не из любви к переменам, а потому что ограничения с обеих сторон делают зарубежный контур ненадёжным. Для вебхука Telegram-бота это означает простую вещь: инфраструктура должна быть предсказуемой, а защита – встроенной, а не собранной из разнородных сервисов. TrafficVeil предлагает переход с Cloudflare, сохраняя привычный набор функций, но без зависимости от внешних ограничений.

Практический итог: сгенерируйте UUID, вынесите вебхук на изолированный эндпоинт, добавьте HMAC-подпись тела с timestamp, проверяйте secret_token, логируйте отказы и держите эндпоинт под облачной защитой с GeoIP-фильтрацией. Эти шаги занимают несколько часов, а закрывают целый класс атак, которые иначе придётся разбирать вручную по логам и восстанавливать данные. Начните с подписи и UUID-пути: это минимальный набор, который даёт максимум эффекта при минимальных изменениях в коде.

Categories: