Protection of Telegram Mini App Against initData Spoofing: Attack Simulation and Backend Validation
window.Telegram.WebApp.initDataUnsafe. This object is created on the client side (inside WebView) and can be easily modified by an attacker via DevTools, proxy servers like Charles/Mitmproxy, or JavaScript injection.

In this article we demonstrate how an attacker can spoof a user ID to gain access rights to someone else's account, and show a complete backend protection chain with HMAC-SHA256 validation, auth_date lifetime limiting, and timing-safe hash comparison. We will also configure the frontend so that MainButton becomes active only after receiving a successful 200 OK response from the authentication endpoint.

Attack Anatomy: why initDataUnsafe is unprotected

The initDataUnsafe object contains parsed information about the user, chat, and launch context. However, the key word here is Unsafe. The Telegram Mini App frontend runs in the user's browser, meaning any variable in client memory can be overwritten.

If your backend accepts requests of the form POST /api/user/balance { userId: 12345 } or trusts raw JSON from the client side, an attacker can change their own ID to an administrator's or a user with a larger balance.

Example payload for reproducing the attack

Below is a client script demonstrating data tampering. The attacker constructs a fake parameter string, injects an arbitrary user.id, and sends it to the server instead of the legitimate Telegram.WebApp.initData:

// Воспроизведение атаки: создание фальшивой строки initData с поддельным user.id
const spoofedUser = {
id: 999999999, // ID целевой жертвы или администратора
first_name: "Attacker",
username: "hacker_account",
is_premium: true
};

// Формируем поддельный payload
const fakeParams = new URLSearchParams({
user: JSON.stringify(spoofedUser),
auth_date: Math.floor(Date.now() / 1000).toString(),
query_id: "AAGXXXXXXXXX",
// Поддельный или произвольный хеш, не совпадающий с подписью Telegram
hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
});

const spoofedInitData = fakeParams.toString();

// Отправляем фальшивые данные на бэкенд
fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ initData: spoofedInitData })
})
.then(res => res.json())
.then(data => console.log('Ответ сервера на фальшивку:', data));

If the server only checks for the presence of the user.id field within the incoming request body, the attacker will slip through. The only way to prevent tampering is to verify the hash signature, created using your bot's private token.

Backend authenticity verification algorithm

To verify the raw initData string, the following algorithm must be implemented:

  1. Parse the query string parameters and extract the hash parameter.
  2. Remove the hash parameter from the list of checked keys.
  3. Sort the remaining key=value pairs alphabetically and join them with a newline character \n. This creates data_check_string.
  4. Calculate секретный ключ: HMAC-SHA256(\"WebAppData\", bot_token).
  5. Calculate the final hash: HMAC-SHA256(data_check_string, secret_key) in hex format.
  6. Compare the obtained hash with the hash from the request using a timing-safe function.
  7. Check the auth_date age to exclude replay attacks.

Implementation of validation in Node.js (Express)

In the following example, a secure signature check is implemented on Node.js using the built-in crypto module. It is important to use crypto.timingSafeEqual to prevent timing attacks:

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

const BOT_TOKEN = process.env.BOT_TOKEN; // Токен бота из getenv
const MAX_AUTH_AGE_SECONDS = 86400; // 24 часа

function verifyInitData(initDataString) {
if (!initDataString) {
return { valid: false, reason: 'initData отсутствует' };
}

const urlParams = new URLSearchParams(initDataString);
const hash = urlParams.get('hash');

if (!hash) {
return { valid: false, reason: 'Хеш не найден в параметрах' };
}

// Удаляем hash перед генерацией data_check_string
urlParams.delete('hash');

// Сортировка ключей по алфавиту
const params = Array.from(urlParams.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`);

const dataCheckString = params.join('
');

// 1. HMAC-SHA256 с константой "WebAppData" и токеном бота
const secretKey = crypto
.createHmac('sha256', 'WebAppData')
.update(BOT_TOKEN)
.digest();

// 2. HMAC-SHA256 от data_check_string с полученным secretKey
const calculatedHash = crypto
.createHmac('sha256', secretKey)
.update(dataCheckString)
.digest('hex');

// Timing-safe сравнение хешей
const calculatedBuffer = Buffer.from(calculatedHash, 'utf8');
const receivedHashBuffer = Buffer.from(hash, 'utf8');

if (calculatedBuffer.length !== receivedHashBuffer.length ||
!crypto.timingSafeEqual(calculatedBuffer, receivedHashBuffer)) {
return { valid: false, reason: 'Невалидная цифровое подпись (HMAC mismatch)' };
}

// Проверка актуальности auth_date
const authDate = parseInt(urlParams.get('auth_date') || '0', 10);
const now = Math.floor(Date.now() / 1000);

if (now - authDate > MAX_AUTH_AGE_SECONDS) {
return { valid: false, reason: 'Срок действия данных истек' };
}

const userRaw = urlParams.get('user');
const user = userRaw ? JSON.parse(userRaw) : null;

return { valid: true, user };
}

app.post('/api/auth', (req, res) => {
const { initData } = req.body;
const verification = verifyInitData(initData);

if (!verification.valid) {
return res.status(401).json({ success: false, error: verification.reason });
}

// Генерация сессии или JWT на основе подлинного user.id
return res.json({
success: true,
user: {
id: verification.user.id,
username: verification.user.username
}
});
});

Alternative implementation of validation in PHP

If your system's backend is written in PHP (Laravel, Yii2 or чистый PHP), the check will proceed similarly using hash_hmac and hash_equals:

<?php

function verifyInitData(string $initData, string $botToken, int $maxAgeSeconds = 86400): array
{
if (empty($initData)) {
return ['valid' => false, 'reason' => 'Пустой initData'];
}

parse_str($initData, $params);

if (!isset($params['hash'])) {
return ['valid' => false, 'reason' => 'Отсутствует hash'];
}

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

// Сортировка по алфавиту
ksort($params);

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

// Вычисление secret_key
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);

// Вычисление финального хеша
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// Timing-safe сравнение
if (!hash_equals($calculatedHash, $hash)) {
return ['valid' => false, 'reason' => 'Подпись не совпадает'];
}

// Проверка срока жизни auth_date
$authDate = (int)($params['auth_date'] ?? 0);
if ((time() - $authDate) > $maxAgeSeconds) {
return ['valid' => false, 'reason' => 'Данные устарели'];
}

$user = isset($params['user']) ? json_decode($params['user'], true) : null;

return ['valid' => true, 'user' => $user];
}

// Пример обработки Webhook / HTTP POST запроса
header('Content-Type: application/json');

$input = json_decode(file_get_contents('php://input'), true);
$initData = $input['initData'] ?? '';
$botToken = getenv('BOT_TOKEN');

$result = verifyInitData($initData, $botToken);

if (!$result['valid']) {
http_response_code(401);
echo json_encode(['error' => $result['reason']]);
exit;
}

echo json_encode(['success' => true, 'userId' => $result['user']['id']]);

Integration with React and MainButton management

A secure Mini App interface should not allow users to perform targeted actions (for example, place an order or confirm a transfer) until the backend confirms session validity.

Below is a React component that keeps MainButton hidden or inactive until receiving a 200 OK response from the /api/auth endpoint:

import React, { useEffect, useState } from 'react';

export function MiniApp() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
const tg = window.Telegram?.WebApp;
if (!tg) return;

tg.ready();
// Скрываем MainButton до успешной авторизации
tg.MainButton.hide();

const rawInitData = tg.initData;

// Отправляем сырую строку initData на валидацию
fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ initData: rawInitData })
})
.then(async (res) => {
if (res.status === 200) {
const data = await res.json();
setIsAuthenticated(true);

// Включаем и настраиваем MainButton строго после HTTP 200
tg.MainButton.setText('ПОДТВЕРДИТЬ ЗАКАЗ');
tg.MainButton.onClick(() => {
console.log('Действие под подлинным userId:', data.user.id);
tg.close();
});
tg.MainButton.show();
} else {
const errData = await res.json();
setError(errData.error || 'Ошибка проверки подписи');
tg.showAlert('Ошибка доступа: данные подделываются или устарели.');
}
})
.catch(() => {
setError('Сетевая ошибка при проверке подписи');
});
}, []);

if (error) {
return <div style={{ color: 'red', padding: 20 }}>Доступ отклонен: {error}</div>;
}

if (!isAuthenticated) {
return <div style={{ padding: 20 }}>Аутентификация и проверка подписи...</div>;
}

return (
<div style={{ padding: 20 }}>
<h1>Защищенная область Mini App</h1>
<p>Сессия успешно подтверждена бэкендом.</p>
</div>
);
}

Security checklist for Telegram Mini Apps development

  • Never use initDataUnsafe for access logic: Only use the raw initData string and validate the signature on the backend.
  • Verify auth_date: Always limit the lifetime of data (for example, no more than 24 hours), so that an intercepted payload cannot be used indefinitely.
  • Use timing-safe comparison: In PHP — hash_equals(), in Node.js — crypto.timingSafeEqual(). This protects against timing attacks when guessing hashes based on the server response.
  • Store BOT_TOKEN securely: The bot token should never appear in the frontend bundle. HMAC verification is performed only on the server.
  • Do not pass critical data through sendData: The method Telegram.WebApp.sendData() simply sends a message to the bot in a chat. For working with the backend, use the standard REST API / GraphQL with session-based token authorization.

If you need help designing a secure architecture Telegram Mini Apps or integrating applications with corporate systems, specialists BotCreator will help implement the project under lock and key.

"}

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.