Тестування Telegram WebApp initData на PHP: синтаксичний аналіз рядка запиту, HMAC-SHA-256 та перевірка auth_date

При передачі даних з додатка Telegram Mini (TWA) на бекенд важливо переконатися, що запит фактично генерується Telegram, а не підробляється зловмисником. Telegram надає об 'єкт initData як рядок запиту, що містить дані користувача, час авторизації та криптографічний хеш. Якщо ви обробляєте ці дані без перевірки підпису, зловмисник зможе надіслати довільний user.id і отримати доступ до чужих рахунків або балансів.

Структура initData та алгоритм автентифікації

Параметр initData є рядком формату, закодованим URL-адресою query_id=...&user=...&auth_date=...&hash=.... Для перевірки підпису на бекенді необхідно виконати послідовність кроків:

  1. Розбір рядка запиту на асоціативний масив параметрів.
  2. Параметр витягу hash і видаліть його з масиву даних, які потрібно перевірити.
  3. Впорядкуйте решту пар ключ-значення в алфавітному порядку за назвами ключів.
  4. Створити рядок data_check_string, де пари об 'єднуються за допомогою символу подачі лінії , n у форматі key=value.
  5. Створити секретный ключ (Secret key) шляхом хешування маркера бота за допомогою HMAC-SHA-256 з використанням постійного рядка WebAppData як ключ.
  6. Обчислити отриманий хеш HMAC-SHA-256 з data_check_string з використанням ПДВ Secret key.
  7. Порівняти отриманий хеш з оригіналом hash від запиту в захищеному від часу режимі.
  8. Перевірте актуальність позначки часу auth_date.

HMAC-SHA-256 створення секретного ключа ТА підпис даних

Особливістю криптографічної схеми Telegram Mini Apps є двоетапне хешування. Токен бота не можна використовувати безпосередньо як ключ для підпису даних. Спочатку створюється проміжний бінарний ключ, де контрольною лінією є літерал ASCII WebAppData.

У PHP функція hash_hmac з аргументом $as_binary = true повертає необроблені двійкові дані, які потім передаються другим аргументом при обчисленні остаточного HMAC. Отриманий хеш видається у вигляді шістнадцяткового рядка нижнього регістру.

<?php

declare(strict_types=1);

/**
* Валидация подписи initData из Telegram Mini App.
*
* @param string $initData Сырая query-строка от Telegram WebApp
* @param string $botToken Токен бота, полученный от @BotFather
* @param int $maxAgeSec Максимальное время жизни подписи в секундах
* @return array{is_valid: bool, data: array<string, mixed>, error: ?string}
*/
function validateTelegramInitData(string $initData, string $botToken, int $maxAgeSec = 86400): array
{
if (trim($initData) === '') {
return ['is_valid' => false, 'data' => [], 'error' => 'InitData is empty'];
}

// Парсинг query-строки без потери структуры JSON внутри параметров
$params = [];
parse_str($initData, $params);

if (!isset($params['hash']) || !is_string($params['hash'])) {
return ['is_valid' => false, 'data' => [], 'error' => 'Hash missing in payload'];
}

$receivedHash = $params['hash'];
unset($params['hash']);

// Сортировка параметров по ключам в алфавитном порядке
ksort($params);

$dataCheckArr = [];
foreach ($params as $key => $value) {
$dataCheckArr[] = $key . '=' . $value;
}
$dataCheckString = implode("
", $dataCheckArr);

// Этап 1: Вычисление secret_key = HMAC_SHA256("WebAppData", bot_token) в бинарном виде
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);

// Этап 2: Вычисление hash = HMAC_SHA256(secret_key, data_check_string) в hex-формате
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// Timing-safe сравнение строк для исключения атаки по времени
if (!hash_equals($calculatedHash, $receivedHash)) {
return ['is_valid' => false, 'data' => [], 'error' => 'Invalid hash signature'];
}

// Проверка наличия и возраста auth_date
if (!isset($params['auth_date']) || !is_numeric($params['auth_date'])) {
return ['is_valid' => false, 'data' => [], 'error' => 'Auth date missing'];
}

$authDate = (int)$params['auth_date'];
if ((time() - $authDate) > $maxAgeSec) {
return ['is_valid' => false, 'data' => [], 'error' => 'InitData standard TTL expired'];
}

// Декодируем объект user из JSON, если он есть
if (isset($params['user']) && is_string($params['user'])) {
$decodedUser = json_decode($params['user'], true);
if (json_last_error() === JSON_ERROR_NONE) {
$params['user'] = $decodedUser;
}
}

return ['is_valid' => true, 'data' => $params, 'error' => null];
}

Захист від хронометражних атак за допомогою HASH_EQUALS()

Звичайне порівняння рядків оператора (===) у PHP вимикається на першому невідповідному байті. Це дозволяє зловмиснику вимірювати мікросекундні затримки відповіді сервера і байт за байтом вибирати дійсний хеш підпису (Timing Attack).

Для усунення цієї вразливості необхідно скористатися функцією hash_equals(). Він виконує порівняння за постійний час, незалежно від того, в якому байті знайдені відмінності, що робить неможливим аналіз часу на стороні клієнта.

Контроль тривалості життя auth_date та запобігання повторним атакам

Успішна перевірка підпису HMAC підтверджує, що дані не були змінені. Однак справжній рядок initData можуть бути перехоплені і відправлені пізніше (Повторна атака). Щоб запобігти повторному використанню застарілих сеансів, обов 'язково перевірте поле auth_date.

Параметр auth_date містить часову позначку Unix часу створення підпису на клієнті. Стандартне вікно терміну дії становить 86 400 секунд (24 години). У критичних операціях, таких як фінансові операції або списання бонусів, рекомендується зменшити TTL до 300–600 секунд.

Обробка запиту на бекенді: контролер авторизації

Нижче наведено практичний приклад точки входу API, яка приймає initData від клієнта, підтверджує підпис і повертає відповідь авторизації або помилку доступу.

<?php

declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

// Получение токена бота из окружения
$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Server environment error']);
exit;
}

// Извлечение JSON-тела запроса
$inputRaw = file_get_contents('php://input');
$payload = json_decode($inputRaw ?: '', true);

if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Invalid JSON payload']);
exit;
}

$initData = $payload['init_data'] ?? null;

if (!is_string($initData) || $initData === '') {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Parameter init_data is required']);
exit;
}

// Валидация initData с TTL в 24 часа
$validationResult = validateTelegramInitData($initData, $botToken, 86400);

if (!$validationResult['is_valid']) {
http_response_code(403);
echo json_encode([
'ok' => false,
'error' => 'Authentication failed: ' . $validationResult['error']
]);
exit;
}

$userData = $validationResult['data']['user'] ?? null;

if (!$userData || !isset($userData['id'])) {
http_response_code(422);
echo json_encode(['ok' => false, 'error' => 'User context missing in initData']);
exit;
}

// Успешный ответ: пользователь верифицирован
echo json_encode([
'ok' => true,
'result' => [
'telegram_id' => $userData['id'],
'first_name' => $userData['first_name'] ?? '',
'username' => $userData['username'] ?? null,
'auth_date' => $validationResult['data']['auth_date']
]
]);

Поширені помилки при синтаксичному аналізі initData

  • Подвійне декодування URL-адрес: Якщо фреймворк або веб-сервер вже декодували рядок запиту шляхом повторного виклику urldecode() зруйнує структуру полів JSON (наприклад, рядки всередині user). Використовуйте необроблений рядок, отриманий від клієнта.
  • Порушення порядку сортування: Сортування ksort() повинні виконуватися ключами після подвійного розділення пар. Значення елементів не слід змінювати до перевірки підпису.
  • Ігнорування двійкового прапорця HMAC: При розрахунку проміжної Secret key Висновок hash_hmac має бути двійковим (аргумент $as_binary = true), інакше ви отримаєте шістнадцятковий рядок, і отриманий хеш не збігатиметься.
  • Дисинхронізація часу сервера: Якщо годинник серверної системи затримується або поспішає, перевірте auth_date може помилково відхилити дійсні запити. Використовуйте синхронізацію NTP на сервері додатків.

Якщо вам потрібна професійна інтеграція міні-додатків Telegram з готовою бекенд-архітектурою, звертайтеся до BotCreator.

Нові статті — у Telegram

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.