При отправке данных из Telegram Mini App на ваш бэкенд единственным источником правды является строка initData. Ее формирует клиентское приложение Telegram и подписывает закрытым ключом бота. Если валидировать данные некорректно или доверять параметрам initDataUnsafe из клиентского JavaScript, злоумышленник сможет подделать user.id, обойти авторизацию или выполнить действия от имени любого пользователя.
В этом материале мы разберем безопасный алгоритм проверки initData на PHP, напишем изолированную функцию валидации и рассмотрим отправку обратного вызова в Telegram API с соблюдением стандартов обработки ошибок.
Анатомия строки initData и алгоритм проверки
Строка initData представляет собой набор пар key=value, разделенных амперсандом (&), аналогично стандартному URL query string. Среди параметров передаются query_id, user (JSON-строка с данными профиля), auth_date (Unix timestamp) и сам hash.
Процесс проверки подлинности состоит из пяти обязательных шагов:
- Извлечение подписи: из полученной строки отделяется параметр
hash. Все остальные параметры остаются для сборки проверочной строки. - Сортировка по алфавиту: оставшиеся пары
key=valueсортируются по ключу в алфавитном порядке (A-Z). - Формирование data_check_string: отсортированные пары объединяются через символ переноса строки (
). - Генерация HMAC-ключа: создается байтовый HMAC-SHA-256 хэш от строки
"WebAppData", где в качестве ключа выступает токен вашего бота. - Сравнение хэшей: вычисляется контрольный HMAC-SHA-256 от
data_check_stringс использованием сгенерированного ключа. Результат сравнивается с полученнымhashчерез функции, устойчивые к атакам по времени (timing attacks).
Пошаговая реализация парсинга и валидации на чистом PHP
При обработке query string нельзя использовать базовый parse_str() без предобработки, так как он декодирует значения и может изменить структуру JSON внутри параметра user, что нарушит итоговую подпись. Надежнее разобрать строку вручную или работать с исходными urldecode-значениями.
Ниже представлен готовый хелпер для проверки подлинности initData с ограничением по времени жизни подписи (по умолчанию — 24 часа):
<?php
declare(strict_types=1);
/**
* Валидирует подпись initData от Telegram Mini App.
*
* @param string $initData Сырая строка window.Telegram.WebApp.initData
* @param string $botToken Токен бота из getenv() или конфига
* @param int $maxAge Максимально допустимый возраст подписи в секундах
* @return array|false Возвращает массив распарсенных данных или false при ошибке
*/
function validateTelegramInitData(string $initData, string $botToken, int $maxAge = 86400): array|false
{
if (empty($initData) || empty($botToken)) {
return false;
}
// 1. Разбираем query string с сохранением исходного кодирования
$rawData = explode('&', $initData);
$params = [];
$receivedHash = '';
foreach ($rawData as $pair) {
if (strpos($pair, '=') === false) {
continue;
}
[$key, $value] = explode('=', $pair, 2);
$decodedKey = urldecode($key);
$decodedValue = urldecode($value);
if ($decodedKey === 'hash') {
$receivedHash = $decodedValue;
} else {
$params[$decodedKey] = $decodedValue;
}
}
if (empty($receivedHash)) {
return false;
}
// 2. Сортируем параметры по алфавиту ключей
ksort($params);
// 3. Формируем data_check_string
$dataCheckArr = [];
foreach ($params as $key => $value) {
$dataCheckArr[] = $key . '=' . $value;
}
$dataCheckString = implode("
", $dataCheckArr);
// 4. Генерируем HMAC-SHA-256 секретный ключ
// Константа 'WebAppData' используется в качестве ключа для хэширования токена бота
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);
// 5. Вычисляем итоговый hash
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
// 6. Timing-safe сравнение хэшей
if (!hash_equals($calculatedHash, $receivedHash)) {
return false;
}
// 7. Проверка срока жизни auth_date
$authDate = isset($params['auth_date']) ? (int)$params['auth_date'] : 0;
if ($authDate === 0 || (time() - $authDate) > $maxAge) {
return false;
}
// Декодируем поле user, если оно присутствовало
if (isset($params['user'])) {
$params['user'] = json_decode($params['user'], true) ?? $params['user'];
}
return $params;
}
Проверка auth_date и защита от Replay Attacks
Даже если контрольный хэш совпадает, уязвимость остается, если подпись можно использовать бесконечно. Злоумышленник, перехвативший чужой initData, сможет отправлять его на ваш сервер с любого устройства.
Проверка auth_date решает эту проблему лишь частично, ограничивая «окно жизни» пакета (например, до 86400 секунд). Чтобы исключить повторное использование подлинных данных внутри этого окна (Replay Attack), необходимо логировать уникальные идентификаторы на стороне бэкенда.
- Сохраняйте
query_idили вычисленныйhashв Redis / DB со временем жизни (TTL), равным допустимому окнуauth_date. - Если полученный
hashуже присутствует в хранилище, отклоняйте запрос с HTTP-кодом403 Forbidden.
Прием запроса от Mini App и работа с Telegram Bot API
Рассмотрим практический эндпоинт, который принимает initData в HTTP-заголовке, проводит валидацию, извлекает ID пользователя и отправляет ему сервисное уведомление через метод sendMessage Bot API с использованием cURL.
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';
if (empty($botToken)) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Bot token is not configured']);
exit;
}
// Извлекаем initData из заголовков или POST-тела
$headers = getallheaders();
$initData = $headers['X-Telegram-Init-Data'] ?? $_POST['init_data'] ?? '';
$validData = validateTelegramInitData($initData, $botToken, 86400);
if ($validData === false) {
http_response_code(401);
echo json_encode(['ok' => false, 'error' => 'Invalid or expired initData']);
exit;
}
$user = $validData['user'] ?? null;
if (!is_array($user) || !isset($user['id'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'User payload missing']);
exit;
}
$telegramUserId = (int)$user['id'];
// Пример безопасной отправки сообщения пользователю через cURL
$telegramApiUrl = "https://api.telegram.org/bot{$botToken}/sendMessage";
$postFields = [
'chat_id' => $telegramUserId,
'text' => "Успешная авторизация в Mini App! Ваш ID: {$telegramUserId}",
'parse_mode' => 'HTML',
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $telegramApiUrl,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($postFields),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
// В продакшене необходимо записать $curlError в лог
http_response_code(502);
echo json_encode(['ok' => false, 'error' => 'Failed to notify user via Telegram API']);
exit;
}
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || !($responseData['ok'] ?? false)) {
http_response_code(502);
echo json_encode(['ok' => false, 'error' => 'Telegram API returned error']);
exit;
}
// Успешный ответ клиенту
echo json_encode([
'ok' => true,
'user_id' => $telegramUserId,
'first_name' => $user['first_name'] ?? '',
]);
Частые ошибки при реализации проверки
Даже с правильным алгоритмом разработчики допускают критические ошибки, приводящие либо к ложным срабатываниям, либо к уязвимостям:
- Забывают аргумент
raw_output = trueв HMAC: Вызовhash_hmac('sha256', $botToken, 'WebAppData')без четвёртого параметраtrueвозвращает hex-строку вместо бинарного массива байтов. Это ломает вычисление финального хэша. - Сравнение через
===вместоhash_equals(): Оператор строгого равенства прерывает сравнение на первом несовпавшем байте, что открывает возможность атаки по времени (Timing Attack) для подбора подписи. - Мутация JSON-строки
user: Если раскодировать JSON из параметраuserдо вычисленияdata_check_stringи собрать его обратно черезjson_encode(), форматирование (пробелы, экранирование слешей) изменится. Подпись перестанет совпадать. - Отсутствие проверки HTTP-кода cURL: При отправке запросов к Telegram API необходимо всегда проверять HTTP-код ответа и статус
json_last_error(), чтобы вовремя обрабатывать таймауты и сетевые сбои.
Если вам нужна комплексная разработка Telegram Mini App и инфраструктуры для обработки заявок, специалисты BotCreator помогут спроектировать архитектуру и защитить API.