initData — это пакет с информацией о пользователе, который Telegram передаёт Mini App при открытии. Без проверки подписи initData любой может подменить user_id и зайти от имени другого человека. Разберём, как корректно валидировать initData на сервере и какие ошибки приводят к компрометации.
Что такое initData
При открытии Mini App Telegram прокидывает в WebView строку initData — закодированный URL-параметр со следующими полями:
user— JSON с информацией о пользователе (id,first_name,last_name,username,language_code,is_premium,photo_url).auth_date— UNIX timestamp выдачи.query_id— идентификатор сессии Mini App.start_param— параметр, переданный вstart_paramпри запуске.chat_type,chat_instance— контекст чата, если Mini App открыт в группе.hash— HMAC-SHA256 подпись всех остальных полей.
Доступен в JS через window.Telegram.WebApp.initData (строка) и window.Telegram.WebApp.initDataUnsafe (распарсенный объект, без проверки!).
Почему нельзя верить initDataUnsafe
initDataUnsafe — это просто распарсенный JSON в JS. Любой может открыть DevTools и переопределить значения. Если бэкенд верит initDataUnsafe.user.id, злоумышленник тривиально входит под чужим аккаунтом.
Правильный путь:
- Mini App шлёт
initData(строку) на бэкенд. - Бэкенд проверяет
hashчерез HMAC-SHA256 секретного ключа, выведенного из токена бота. - Только после проверки доверяет
user.id.
Алгоритм проверки
Псевдокод:
- Принять
initDataкак строку. - Распарсить query string в map ключ-значение.
- Извлечь
hash, остальные поля собрать вdata_check_string(отсортировать ключи, форматkey=value, соединить через\n). - Вычислить
secret_key = HMAC_SHA256("WebAppData", bot_token). - Вычислить
expected_hash = HMAC_SHA256(secret_key, data_check_string). - Сравнить с
hashизinitDataв hex. - Если совпадает — данные подлинные.
На Python:
import hmac, hashlib
from urllib.parse import parse_qsl
def verify_init_data(init_data: str, bot_token: str) -> bool:
parsed = dict(parse_qsl(init_data, strict_parsing=True))
received_hash = parsed.pop("hash")
data_check_string = "\n".join(
f"{k}={v}" for k, v in sorted(parsed.items())
)
secret_key = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
expected_hash = hmac.new(secret_key, data_check_string.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(received_hash, expected_hash)
На Node — то же самое через crypto.createHmac.
Срок жизни
auth_date показывает, когда initData был выдан. Telegram не отзывает старые initData, поэтому проверять «не слишком ли старый» — обязанность сервера.
Стандартная практика — отбрасывать initData старше 1–24 часов:
if int(parsed["auth_date"]) + 86400 < time.time():
raise ValueError("initData expired")
Без этой проверки украденный initData живёт вечно. Особенно опасно при логировании — если initData попал в логи, он навсегда годен для входа.
Где проверять
Каждый запрос к бэкенду от Mini App должен содержать initData и проверяться. Не делайте «проверил один раз при логине, выдал JWT, дальше доверяю» — JWT без обновления тоже становится украденным токеном.
Реалистичные паттерны:
- Каждый запрос проверяет
initDataсвежий (валидация менее 5 минут раздумий не отнимает). - Сессионный токен выдаётся после первой проверки, живёт час, потом перезапрашивается через свежий
initData.
Типичные ошибки
Доверие user.id из JS
// Неправильно
const userId = Telegram.WebApp.initDataUnsafe.user.id;
fetch("/api/me?user_id=" + userId);
Правильно — user_id достаётся на бэкенде из проверенного initData, не из параметров запроса.
Использование initData в URL
initData не должен идти в query string GET-запросов: попадает в логи, в Referer, в браузерную историю. Передавайте через заголовок Authorization: tma <initData> или в JSON-теле POST.
Сортировка ключей через split
Bug in many tutorials: разделяют по & и обнуляют значения с & внутри (например, start_param=foo&bar). Используйте библиотеку для парсинга query string, не самописный split.
Не сравнивать через constant-time
if (received_hash == expected_hash) уязвим к timing attack. Используйте hmac.compare_digest (Python), crypto.timingSafeEqual (Node).
Хардкод bot_token
Токен — секрет. Не вшивайте в код, используйте переменные окружения. Если токен утечёт, любая проверка initData ломается, потому что злоумышленник сможет сам подписывать.
Защита от replay
Если злоумышленник перехватил initData, он может отправлять одни и те же запросы. Замедлить:
auth_dateменее 1–24 часов.- Дополнительный
nonceв запросе — уникальный токен, выдаваемый бэкендом, и обязательный к передаче в следующий запрос. - Привязка к IP — для критичных операций.
Привязка пользователя
После проверки initData берёте user.id (числовой). Это и есть стабильный идентификатор пользователя в Telegram. По нему ведёте профиль, привязываете к платежам, состояниям FSM и т.д.
username может меняться. first_name, last_name — могут меняться. photo_url — временный.
Mini App на чужом домене
Mini App запускается с домена, который вы зарегистрировали в BotFather через /setdomain. Telegram проверяет, что URL входит в whitelist. Это защищает от того, чтобы кто-то открыл вашу Mini App на постороннем сайте — initData для другого домена выдан не будет.
Для собственного бэкенда настройте CORS только под ваши домены. Не открывайте API всем подряд.
Логирование
Что можно логировать:
user_id(числовой).query_id(одноразовый, безопасен).auth_date.
Что нельзя:
- Полный
initData. hash.- Любые токены и подписи.
Утечка лога с initData = массовая компрометация.
Итого
initData — это подписанный пакет данных пользователя, который Telegram передаёт Mini App. Без проверки HMAC доверять ему нельзя — initDataUnsafe подделывается тривиально. Алгоритм: парсим query string, собираем data_check_string отсортированно, считаем HMAC секретного ключа от токена бота, сравниваем через constant-time. Дополнительно проверяем auth_date на свежесть. Не пихайте initData в URL и логи, не выдавайте долгоживущие сессионные токены без проверки. Эти простые правила защищают Mini App от 99% атак на аутентификацию.
Частые вопросы
Что такое initData в Telegram Mini App?
При открытии Mini App Telegram прокидывает в WebView строку initData — закодированный URL-параметр со следующими полями. user — JSON с информацией о пользователе (id, first_name, last_name, username, language_code, is_premium, photo_url). auth_date — UNIX timestamp выдачи. query_id — идентификатор сессии Mini App. start_param — параметр, переданный при запуске. chat_type, chat_instance — контекст чата если в группе. hash — HMAC-SHA256 подпись всех остальных полей. Доступен в JS через window.Telegram.WebApp.initData (строка) и initDataUnsafe (распарсенный объект, без проверки).
Почему нельзя доверять initDataUnsafe в JavaScript?
initDataUnsafe — это просто распарсенный JSON в JS. Любой может открыть DevTools и переопределить значения. Если бэкенд верит initDataUnsafe.user.id, злоумышленник тривиально входит под чужим аккаунтом. Правильный путь. Mini App шлёт initData (строку) на бэкенд. Бэкенд проверяет hash через HMAC-SHA256 секретного ключа, выведенного из токена бота. Только после проверки доверяет user.id. Любой запрос от Mini App, делающий что-то от имени пользователя, должен сопровождаться проверенным initData на сервере.
Как алгоритмически проверить initData на сервере?
Алгоритм проверки. Принять initData как строку. Распарсить query string в map ключ-значение. Извлечь hash, остальные поля собрать в data_check_string (отсортировать ключи, формат key=value, соединить через \n). Вычислить secret_key = HMAC_SHA256("WebAppData", bot_token). Вычислить expected_hash = HMAC_SHA256(secret_key, data_check_string). Сравнить с hash из initData в hex через constant-time (hmac.compare_digest, crypto.timingSafeEqual), не == чтобы избежать timing attack. Если совпадает — данные подлинные.
Какой срок жизни должен быть у initData в Mini App?
auth_date показывает, когда initData был выдан. Telegram не отзывает старые initData, поэтому проверять «не слишком ли старый» — обязанность сервера. Стандартная практика — отбрасывать initData старше 1–24 часов. Без этой проверки украденный initData живёт вечно. Особенно опасно при логировании — если initData попал в логи, он навсегда годен для входа. Также не пихайте initData в URL GET-запросов: попадает в логи, в Referer, в браузерную историю. Передавайте через заголовок Authorization: tma INIT_DATA или в JSON-теле POST.
Какие типичные ошибки при работе с initData?
Пять частых ошибок. Доверие user.id из JS — userId из initDataUnsafe передаётся в URL, бэкенд ему верит; правильно — user_id достаётся на бэкенде из проверенного initData. Использование initData в URL — попадает в логи и Referer, передавайте через заголовок. Сортировка ключей через split — bug in many tutorials, используйте библиотеку для парсинга. Не сравнивать через constant-time — уязвим к timing attack. Хардкод bot_token в коде — токен секрет, используйте переменные окружения.
Как защититься от replay-атак на initData в Mini App?
Если злоумышленник перехватил initData, он может отправлять одни и те же запросы. Замедлить. Auth_date менее 1–24 часов. Дополнительный nonce в запросе — уникальный токен, выдаваемый бэкендом, и обязательный к передаче в следующий запрос. Привязка к IP — для критичных операций. Также Mini App запускается с домена, который вы зарегистрировали в BotFather через /setdomain. Telegram проверяет, что URL входит в whitelist. Это защищает от того, чтобы кто-то открыл вашу Mini App на постороннем сайте — initData для другого домена выдан не будет.