Telegram Mini Apps (ранее Web Apps) предоставляют мощный инструмент для создания интерактивных веб-интерфейсов внутри Telegram. Однако, как и в любом веб-приложении, критически важно обеспечить безопасность данных, передаваемых от клиента к серверу. Одним из ключевых элементов этой безопасности является проверка initData — строки, содержащей информацию о пользователе, боте и самом Mini App, которая передаётся при запуске. В этой статье мы подробно разберём, как валидировать initData на PHP, чтобы защитить ваше приложение от поддельных запросов.
Что такое initData и зачем её проверять?
initData — это строка, которую Telegram WebApp SDK генерирует и передаёт вашему веб-приложению. Она содержит различные параметры, такие как user, chat, query_id, auth_date и, самое главное, hash. Этот hash используется для проверки целостности и подлинности всей остальной части initData. Если вы не проверите initData, злоумышленник может легко подделать данные пользователя, выдать себя за другого или отправить некорректные запросы, что приведёт к уязвимостям в вашем приложении.
Извлечение и подготовка данных
Первым шагом является получение строки initData из запроса (обычно это POST-параметр или заголовок) и её разбор. initData представляет собой URL-кодированную строку запроса, которую необходимо преобразовать в ассоциативный массив.
<?php
/**
* Валидирует initData Telegram Mini Apps.
* @param string $initDataRaw Строка initData, полученная от клиента.
* @param string $botToken Токен вашего Telegram-бота.
* @param int $maxAuthDateLifetimeSeconds Максимальное время жизни auth_date в секундах.
* @return array|false Ассоциативный массив с разобранными данными или false в случае ошибки валидации.
*/
function validateTelegramInitData(string $initDataRaw, string $botToken, int $maxAuthDateLifetimeSeconds = 3600)
{
// 1. Разбираем query string
parse_str($initDataRaw, $parsedData);
if (!isset($parsedData['hash'])) {
// Хеш отсутствует, данные невалидны
return false;
}
$hash = $parsedData['hash'];
unset($parsedData['hash']); // Удаляем хеш для дальнейшей обработки
// 2. Сортируем параметры по ключу и формируем строку для хеширования
ksort($parsedData);
$dataCheckString = [];
foreach ($parsedData as $key => $value) {
$dataCheckString[] = $key . '=' . $value;
}
$dataCheckString = implode("\n", $dataCheckString);
// 3. Проверяем auth_date
if (!isset($parsedData['auth_date']) || !is_numeric($parsedData['auth_date'])) {
// auth_date отсутствует или некорректен
return false;
}
$authDate = (int)$parsedData['auth_date'];
if (time() - $authDate > $maxAuthDateLifetimeSeconds) {
// Данные устарели
return false;
}
// 4. Возвращаем разобранные данные для дальнейшего использования
return $parsedData;
}
// Пример использования (в реальном приложении токен должен быть из getenv или params)
$initDataFromClient = $_POST['initData'] ?? ''; // Или $_GET['initData'] или из заголовка
$botToken = getenv('TELEGRAM_BOT_TOKEN'); // Получаем токен из переменных окружения
if (empty($initDataFromClient) || empty($botToken)) {
// Обработка отсутствия данных или токена
header('HTTP/1.1 400 Bad Request');
echo json_encode(['error' => 'Missing initData or bot token.']);
exit;
}
$validatedData = validateTelegramInitData($initDataFromClient, $botToken);
if ($validatedData === false) {
header('HTTP/1.1 403 Forbidden');
echo json_encode(['error' => 'Invalid initData.']);
exit;
}
// Данные валидны, можно использовать $validatedData
// Например, для аутентификации пользователя или сохранения данных
echo json_encode(['status' => 'success', 'data' => $validatedData]);
?>
Генерация секретного ключа из токена бота
Согласно документации Telegram, секретный ключ для хеширования получается путём применения SHA256 к строке 'WebAppData' с использованием токена бота в качестве ключа. Этот ключ затем используется для HMAC-SHA256 хеширования data_check_string.
<?php
/**
* Валидирует initData Telegram Mini Apps.
* @param string $initDataRaw Строка initData, полученная от клиента.
* @param string $botToken Токен вашего Telegram-бота.
* @param int $maxAuthDateLifetimeSeconds Максимальное время жизни auth_date в секундах.
* @return array|false Ассоциативный массив с разобранными данными или false в случае ошибки валидации.
*/
function validateTelegramInitData(string $initDataRaw, string $botToken, int $maxAuthDateLifetimeSeconds = 3600)
{
parse_str($initDataRaw, $parsedData);
if (!isset($parsedData['hash'])) {
return false;
}
$hash = $parsedData['hash'];
unset($parsedData['hash']);
ksort($parsedData);
$dataCheckString = [];
foreach ($parsedData as $key => $value) {
$dataCheckString[] = $key . '=' . $value;
}
$dataCheckString = implode("\n", $dataCheckString);
// Проверка auth_date
if (!isset($parsedData['auth_date']) || !is_numeric($parsedData['auth_date'])) {
return false;
}
$authDate = (int)$parsedData['auth_date'];
if (time() - $authDate > $maxAuthDateLifetimeSeconds) {
return false;
}
// 5. Генерируем секретный ключ
$secretKey = hash_hmac('sha256', 'WebAppData', $botToken, true);
// 6. Вычисляем хеш на стороне сервера
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
// 7. Сравниваем полученный хеш с вычисленным (timing-safe)
if (!hash_equals($hash, $calculatedHash)) {
return false;
}
return $parsedData;
}
// Пример использования в Laravel контроллере
// app/Http/Controllers/WebAppController.php
// use Illuminate\Http\Request;
// use Illuminate\Support\Facades\Log;
// class WebAppController extends Controller
// {
// public function processInitData(Request $request)
// {
// $initDataRaw = $request->input('initData');
// $botToken = config('services.telegram_bot_api.token'); // Из config/services.php
// if (empty($initDataRaw) || empty($botToken)) {
// Log::warning('Missing initData or bot token.', ['initData' => $initDataRaw]);
// return response()->json(['error' => 'Missing initData or bot token.'], 400);
// }
// $validatedData = validateTelegramInitData($initDataRaw, $botToken);
// if ($validatedData === false) {
// Log::warning('Invalid initData received.', ['initData' => $initDataRaw]);
// return response()->json(['error' => 'Invalid initData.'], 403);
// }
// // Данные валидны, можно использовать $validatedData
// // Например, для аутентификации пользователя или сохранения данных
// Log::info('InitData successfully validated.', ['user_id' => $validatedData['user']['id'] ?? 'N/A']);
// return response()->json(['status' => 'success', 'data' => $validatedData]);
// }
// }
?>
Timing-safe сравнение хешей
Обычное сравнение строк с помощью оператора == или === может быть уязвимо для атак по времени (timing attacks). Злоумышленник может измерять время выполнения сравнения, чтобы угадать символы хеша по одному. Для предотвращения этого используйте функцию hash_equals(), которая сравнивает строки за постоянное время, независимо от того, на каком символе они начинают различаться.
Проверка auth_date
Параметр auth_date содержит метку времени (Unix timestamp) запуска Mini App. Крайне важно проверять, что эта метка времени не слишком старая. Если не установить ограничение на время жизни auth_date, злоумышленник может перехватить валидный initData и использовать его повторно спустя долгое время. Рекомендуется устанавливать максимальное время жизни auth_date в пределах нескольких минут (например, 1 час или меньше), чтобы минимизировать риск атак повторного воспроизведения.
Обработка ошибок и логирование
В случае любой ошибки валидации (отсутствие хеша, некорректный auth_date, несовпадение хешей) ваше приложение должно отклонить запрос и вернуть соответствующий HTTP-статус (например, 403 Forbidden или 400 Bad Request). Важно также логировать эти события, чтобы отслеживать потенциальные попытки атак или ошибки в работе клиентской части.
Использование в фреймворках (Yii2, Laravel)
В фреймворках, таких как Yii2 или Laravel, вы можете инкапсулировать логику валидации в отдельный сервис, компонент или даже middleware. Это позволит легко переиспользовать код и поддерживать чистоту архитектуры.
Пример для Yii2
<?php
namespace app\components;
use Yii;
use yii\base\Component;
class TelegramWebAppValidator extends Component
{
public $botToken; // Указывается в конфигурации приложения
public $maxAuthDateLifetimeSeconds = 3600; // 1 час по умолчанию
public function validateInitData(string $initDataRaw): array|false
{
if (empty($this->botToken)) {
Yii::error('Telegram bot token is not configured.', __METHOD__);
return false;
}
parse_str($initDataRaw, $parsedData);
if (!isset($parsedData['hash'])) {
Yii::warning('InitData: hash is missing.', __METHOD__);
return false;
}
$hash = $parsedData['hash'];
unset($parsedData['hash']);
ksort($parsedData);
$dataCheckString = [];
foreach ($parsedData as $key => $value) {
$dataCheckString[] = $key . '=' . $value;
}
$dataCheckString = implode("\n", $dataCheckString);
if (!isset($parsedData['auth_date']) || !is_numeric($parsedData['auth_date'])) {
Yii::warning('InitData: auth_date is missing or invalid.', __METHOD__);
return false;
}
$authDate = (int)$parsedData['auth_date'];
if (time() - $authDate > $this->maxAuthDateLifetimeSeconds) {
Yii::warning('InitData: auth_date is too old. Timestamp: ' . $authDate, __METHOD__);
return false;
}
$secretKey = hash_hmac('sha256', 'WebAppData', $this->botToken, true);
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
if (!hash_equals($hash, $calculatedHash)) {
Yii::warning('InitData: hash mismatch. Provided: ' . $hash . ', Calculated: ' . $calculatedHash, __METHOD__);
return false;
}
return $parsedData;
}
}
// В конфигурации web.php:
/*
'components' => [
'telegramWebAppValidator' => [
'class' => 'app\components\TelegramWebAppValidator',
'botToken' => getenv('TELEGRAM_BOT_TOKEN'),
'maxAuthDateLifetimeSeconds' => 1800, // 30 минут
],
// ... другие компоненты
],
*/
// В контроллере:
/*
class MyWebAppController extends \yii\web\Controller
{
public function actionProcessData()
{
$initDataRaw = Yii::$app->request->post('initData');
$validatedData = Yii::$app->telegramWebAppValidator->validateInitData($initDataRaw);
if ($validatedData === false) {
Yii::$app->response->statusCode = 403;
return ['error' => 'Invalid initData.'];
}
// ... обработка валидных данных
return ['status' => 'success', 'data' => $validatedData];
}
}
*/
?>
Заключение
Валидация initData — это обязательный шаг при разработке безопасных Telegram Mini Apps. Следуя описанным выше шагам, вы сможете надёжно проверять подлинность данных, предотвращать подделки и обеспечивать целостность вашего приложения. Не забывайте всегда получать токен бота из безопасного источника (переменные окружения, конфигурационные файлы), а не хранить его в открытом коде.
Для создания мощных и безопасных Telegram-ботов и Mini Apps, обратитесь к профессионалам BotCreator на botservice.biz.