MAX Messenger: Безопасный Webhook и проверка initData
Реализация HMAC-SHA256 валидации сессии мини-приложения и timingSafeEqual для вебхука
Обновлено: Октябрь 2026•Время чтения: 5 мин•
#MAX#Mini-App#Webhook#Security#Crypto#TypeScript
Краткая суть (TL;DR)
Готовые сниппеты на TypeScript для верификации входящего вебхука бота MAX и безопасной проверки сессии мини-приложения без доверия клиенту.
1. Валидация initData мини-приложения (Node.js / Next.js)
Алгоритм: парсинг query-строки, сортировка ключей по алфавиту без поля 'hash', HMAC-SHA256 ключом 'WebAppData' + BotToken, и безопасное сравнение хешей.
typescript
import { createHmac, timingSafeEqual } from "node:crypto";
export function validateMaxInitData(initData: string, botToken: string) {
const params = new URLSearchParams(initData);
const hash = params.get("hash");
if (!hash) return null;
params.delete("hash");
const dataCheckString = Array.from(params.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join("\n");
const secret = createHmac("sha256", "WebAppData").update(botToken).digest();
const calculatedHash = createHmac("sha256", secret).update(dataCheckString).digest("hex");
const a = Buffer.from(calculatedHash, "hex");
const b = Buffer.from(hash, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
const authDate = Number(params.get("auth_date"));
const now = Math.floor(Date.now() / 1000);
if (now - authDate > 86400) return null; // старше 24 часов
return JSON.parse(params.get("user") || "{}");
}Важные нюансы и рекомендации:
- ◆Никогда не доверяйте initDataUnsafe на бэкенде — клиентский объект легко подделать.
- ◆Всегда проверяйте auth_date, чтобы предотвратить атаки повторного воспроизведения (replay attack).
2. Секретный заголовок вебхука X-Max-Bot-Api-Secret
MAX передает заданный вами секрет в заголовке x-max-bot-api-secret. Защищайтесь от атак по времени (timing attacks):
typescript
export function verifyWebhookSecret(incoming: string | null, expected: string): boolean {
if (!incoming || !expected) return false;
const a = Buffer.from(incoming);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}