← Вся база знаний/Интеграции & API

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);
}