Любой публичный бот в MAX рано или поздно попадает под атаку: парсинг каталога, спам в формы, перебор номеров заказа, попытка вывести бот в неконсистентное состояние, накрутка регистраций ради пустой базы. MAX — российский мессенджер от VK Tech, и хотя площадка молодая, паттерны атак наследуются из Telegram и WhatsApp: те же сборщики chat_id, те же фрод-сценарии в платежах, те же боты-парсеры на головных эндпоинтах. Хорошая антибот-защита — это не одна капча на старте, а слоёная оборона, которая работает незаметно для нормального пользователя и больно бьёт по автоматике.
В этом посте — карта реальных угроз для бота в MAX и проверенный набор контрмер: от CAPTCHA при /start до WAF на webhook, поведенческой аналитики, идемпотентности платежей и security-канала с алертами. Все примеры — из продовых ботов, которые успели поймать на себе как минимум одну массовую атаку.
Карта угроз: что реально атакуют в MAX
Угрозы делятся на три класса: технические (DDoS, парсинг), экономические (накрутка, фрод платежей) и репутационные (спам в группах, фишинг). Ниже — сводная таблица того, что встречается чаще всего у клиентских ботов в MAX.
| Угроза | Цель атакующего | Что страдает |
|---|---|---|
Накрутка /start | Раздуть базу, сорвать аналитику | CRM, маркетинг, биллинг провайдера |
| Scrape базы пользователей | Собрать ID для последующего спама | Пользователи, 152-ФЗ |
| Abuse платных функций | Купить за бесплатно, double-spend | Касса, баланс провайдеров |
| DDoS на webhook | Положить сервис | Доступность, SLA |
| Спам в групповых чатах | Реклама/фишинг под видом бота | Репутация, удаление из чатов |
| Фишинг через бота | Угнать аккаунты у юзеров | Доверие, юр-риски |
| Перебор номеров заказа | Прочитать чужие заказы | Утечка ПДн, штрафы РКН |
| Заваливание формы заявок | Парализовать менеджеров | Конверсия, фонд оплаты труда |
| Prompt-injection (LLM-боты) | Получить системный промпт | Конфиденциальность, биллинг LLM |
Дальше — слой за слоем, от входной точки до бэкенда.
CAPTCHA при /start: отсеиваем сборщиков
Основная цель массового флуда /start — собрать как можно больше валидных идентификаторов чатов, которым потом можно спамить. CAPTCHA внутри бота снижает «дешевизну» атаки на порядок: бот-сборщик должен либо обучить решатель, либо подключить antigate-сервис, что уже стоит денег.
В MAX доступны три формата CAPTCHA внутри бота:
- Кнопочная — простая арифметика, выбор «лишнего» элемента, выбор страны.
- Текст с картинки — рендерим PNG с искажённым числом, ответ вводится текстом.
- Внешняя через Mini App или URL — Yandex SmartCaptcha на отдельной странице, callback возвращает токен в бот.
Минимальный вариант — кнопочная CAPTCHA с TTL 60 секунд:
import random, time
from max_bot import Router, Message, InlineKeyboardButton, InlineKeyboardMarkup
router = Router()
PENDING = {} # chat_id -> (correct, expires_at, attempts)
@router.message(command="start")
async def on_start(msg: Message):
a, b = random.randint(2, 9), random.randint(2, 9)
correct = a + b
options = list({correct, correct + 1, correct - 2, correct + 3})
random.shuffle(options)
kb = InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text=str(o), callback_data=f"cap:{o}")
for o in options
]])
PENDING[msg.chat.id] = (correct, time.time() + 60, 0)
await msg.answer(
f"Подтвердите, что вы человек: сколько будет {a} + {b}?",
reply_markup=kb,
)
Если пользователь не нажал в течение TTL или ошибся трижды — мягкий бан на 24 часа. Реальные пользователи проходят с первого раза, сборщики отваливаются.
Yandex SmartCaptcha — лучший выбор для РФ
Для серьёзных сценариев (форма заявки, регистрация в личном кабинете, отправка промокода) кнопочной CAPTCHA недостаточно — она ломается дешёвым ML-классификатором. В этом случае подключаем Yandex SmartCaptcha: российский сервис, не требует согласия Google reCAPTCHA, дружит с РКН и работает быстро.
Интеграция в MAX-бот делается через два пути:
- Mini App внутри бота — отдельная страница
/captcha?token=<one-time>, на которой рендерится виджет SmartCaptcha. После прохождения фронт шлёт токен на бэкенд бота, бот валидирует черезhttps://smartcaptcha.yandexcloud.net/validate. - Внешний URL — бот шлёт ссылку, пользователь открывает в браузере, после прохождения видит «Возвращайтесь в бот».
Серверная валидация:
import httpx, os
SMARTCAPTCHA_SECRET = os.environ["SMARTCAPTCHA_SERVER_KEY"]
async def verify_smartcaptcha(token: str, ip: str) -> bool:
if not SMARTCAPTCHA_SECRET:
return token == "dev-no-captcha" # dev-режим
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.post(
"https://smartcaptcha.yandexcloud.net/validate",
data={"secret": SMARTCAPTCHA_SECRET, "token": token, "ip": ip},
)
if r.status_code != 200:
return False
return r.json().get("status") == "ok"
Виджет на стороне Mini App подгружается одним скриптом и вызывает callback с токеном — токен живёт минуту, поэтому валидируем сразу. В продовом режиме без секрета считаем все токены невалидными, в dev-режиме разрешаем литерал dev-no-captcha для локальных тестов.
Rate limiting per user через Redis Lua
Token bucket на уровне chat_id — обязательный слой. Без него один пользователь может за секунду отправить 30 callback-ов и положить вашу очередь обработки. Решение — атомарный Lua-скрипт в Redis (избегаем гонок и round-trip-ов):
-- KEYS[1] = bucket key, ARGV = capacity, refill_per_sec, now_ms, cost
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local data = redis.call("HMGET", key, "tokens", "ts")
local tokens = tonumber(data[1]) or capacity
local ts = tonumber(data[2]) or now
local delta = (now - ts) / 1000.0 * refill
tokens = math.min(capacity, tokens + delta)
if tokens < cost then
redis.call("HMSET", key, "tokens", tokens, "ts", now)
redis.call("PEXPIRE", key, 60000)
return 0
end
tokens = tokens - cost
redis.call("HMSET", key, "tokens", tokens, "ts", now)
redis.call("PEXPIRE", key, 60000)
return 1
Использование из Python:
async def allow(chat_id: int, cost: int = 1) -> bool:
res = await redis.evalsha(
SHA, 1, f"rl:user:{chat_id}",
20, # capacity (burst)
2, # refill per sec
int(time.time() * 1000),
cost,
)
return bool(res)
Дорогие операции (создание заявки, генерация LLM-ответа, отправка СМС) проходят с cost=5 — они быстрее съедают бюджет. Жёстко банить за превышение не стоит — у нормального пользователя могут быть всплески (нажал несколько кнопок подряд), мягкий ответ «слишком быстро, подождите» и игнорирование апдейтов на следующие 5 секунд.
Rate limiting per IP на webhook
MAX отправляет апдейты со своих IP-адресов (диапазоны на момент написания не публикуются официально, но фиксируются эмпирически). В реальности webhook доступен всему интернету, и если кто-то узнал ваш URL (а он рано или поздно засветится в логах CDN или скрине ошибки), его начнут долбить запросами без подписи.
Лимит на IP должен стоять до бизнес-логики — на уровне Nginx или edge:
limit_req_zone $binary_remote_addr zone=max_webhook:10m rate=30r/s;
location /max/webhook {
limit_req zone=max_webhook burst=60 nodelay;
limit_req_status 429;
proxy_pass http://app_upstream;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Max-Bot-Api-Secret-Token $http_x_max_bot_api_secret_token;
}
Параллельно — IP-allowlist MAX (как только VK Tech опубликует диапазоны) или WAF (Cloudflare, Yandex Smart Web Security). Это убирает 90% мусорного трафика до того, как он касается приложения.
secret_token: подпись webhook от MAX
Bot API MAX позволяет задать секретный токен при setWebhook. Сервер MAX присылает его в заголовке X-Max-Bot-Api-Secret-Token — всё, что без него или с неверным, отбрасываем.
import hmac, os
from fastapi import FastAPI, Header, HTTPException, Request
SECRET = os.environ["MAX_WEBHOOK_SECRET"]
app = FastAPI()
@app.post("/max/webhook")
async def webhook(
req: Request,
x_max_bot_api_secret_token: str = Header(default=""),
):
if not hmac.compare_digest(x_max_bot_api_secret_token, SECRET):
raise HTTPException(status_code=403, detail="bad secret")
update = await req.json()
await dispatcher.feed_raw_update(update)
return {"ok": True}
Сравнение строго через hmac.compare_digest — иначе timing attack теоретически позволит подобрать секрет посимвольно. Параллельно: HTTPS обязателен (MAX не отправит на HTTP), setWebhook вызывается из CI с проверкой текущего URL, ротация секрета — раз в квартал и при каждом увольнении из команды.
Поведенческая аналитика и MAX-сигналы
Не все боты тупые. Серьёзный сборщик решит CAPTCHA через antigate, пройдёт rate-limit и будет дозированно собирать базу. Здесь спасают сигналы аккаунта и паттерны поведения, которые автоматизировать дороже.
MAX-специфичные сигналы, которые стоит логировать на каждом первом контакте:
- возраст аккаунта — эвристика по числовому
user_id(свежие ID растут монотонно, есть открытые маркеры); - наличие фото профиля и заполненного имени;
- верифицирован ли номер телефона (флаг в
User); - страна по
language_codeили геозоне привязки номера; username— пустой ник + свежий ID + нет фото = подозрительно.
Поведенческие паттерны:
- много
/startподряд без интерактива в течение часа; - ноль текстовых сообщений, только нажатия кнопок;
- регистрация из IP дата-центров (если есть Mini App с
initData); - аномально быстрые ответы (меньше 200 мс на сложный экран);
- хождение строго по «золотому пути» сценария без ошибок и возвратов.
Простая scoring-модель:
def risk_score(user, signals) -> int:
score = 0
if not user.username: score += 10
if not signals["has_photo"]: score += 10
if not user.phone_verified: score += 15
if signals["account_age_days"] < 1: score += 20
if signals["lang"] != signals["geo_lang"]: score += 15
if signals["start_count_24h"] > 5: score += 30
if signals["msg_to_callback_ratio"] < 0.05: score += 25
return score
# >= 50 — shadow-ban; 30..49 — повторная CAPTCHA; < 30 — пропускаем
Shadow-ban — отдельный приём: пользователю отвечаем как будто всё нормально, но действия в реальности не выполняются. Атакующий не понимает, что забанен, и не пересоздаёт аккаунт.
Фильтрация по поведению на старте
Когда новый пользователь нажимает /start, можно сразу применить лёгкие проверки, которые ловят 80% автоматических парсеров:
- Honey-pot кнопка — невидимая для человека (например, с эмодзи нулевой ширины), но скрипт её нажмёт по индексу. Помечаем как бота.
- Подождать 1.5–2 секунды перед отправкой первого ответа — нормальный пользователь не торопится. Скрипт за это время уже пришлёт следующий апдейт, и мы увидим аномалию.
- Простой вопрос — «выберите страну», «нажмите красную кнопку».
Это не остановит мотивированного злоумышленника, но сильно поднимает стоимость атаки.
Anti-spam в групповых чатах MAX
Если бот — админ группы в MAX, на нём и обязанность чистить мусор. Минимальный набор:
- словарный фильтр (мат, спам-фразы, сезонные кампании);
- проверка ссылок (whitelist доменов или scoring через внешний API);
- CAPTCHA для новых участников (кнопочная, с TTL и киком при провале);
- лимит на N сообщений в M секунд от одного юзера;
- автокик за
chat_join_requestбез последующей активности 24 часа.
Пример декоратора для антиспам-проверки:
import re
from functools import wraps
SPAM_WORDS = {"казино", "крипто-сигналы", "1xbet", "заработок без вложений"}
LINK_RE = re.compile(r"https?://([^\s/]+)")
WHITELIST = {"max.ru", "vk.com", "company.ru"}
def antispam(handler):
@wraps(handler)
async def wrapper(msg, *args, **kwargs):
text = (msg.text or msg.caption or "").lower()
if any(w in text for w in SPAM_WORDS):
await msg.delete()
await mute_user(msg.chat.id, msg.from_user.id, hours=24)
return
for host in LINK_RE.findall(text):
if host not in WHITELIST:
await msg.delete()
return
return await handler(msg, *args, **kwargs)
return wrapper
Ловушка: не удаляйте сообщения админов и доверенных аккаунтов — иначе бот будет выгребать собственные напоминания и напоминания модераторов.
Защита от парсинга базы пользователей
Если в боте есть «найти пользователя по нику» или «показать профиль соседа» — это потенциальный enum-эндпоинт. Атакующий за ночь соберёт всю базу.
Меры:
- никогда не возвращайте инфу о других пользователях по числовому
id(только по их же действиям, в их сессии); - рандомизируйте задержки ответов на «поиск» — добавьте случайные 200–800 мс;
- не отвечайте «не найдено» — отвечайте идентично «найдено, но скрыто», чтобы сборщик не отделял существующие ID от несуществующих;
- лимит на N запросов поиска в сутки;
- длинные UUID вместо инкрементальных номеров заказов и инвойсов.
import asyncio, random
async def search_user_by_nick(actor_id, query):
if not await allow(actor_id, cost=3):
return GENERIC_LIMIT_RESPONSE
await asyncio.sleep(random.uniform(0.2, 0.8))
user = await db.find(query)
# одинаковый ответ для found / not found
return GENERIC_PROFILE_HIDDEN
Защита платных функций: idempotency и double-spend
Любое действие, которое тратит деньги (купить подписку, заказать доставку, списать с баланса), должно быть идемпотентным. Иначе ретрай webhook от MAX задвоит заказ, а атакующий сможет «параллельно» дважды списать одну транзакцию через гонку.
async def charge_and_grant(user_id: int, item_id: str, amount: int, idem_key: str):
async with db.transaction():
# 1. блокируем idempotency-ключ
existing = await db.fetchrow(
"INSERT INTO payments (idem_key, user_id, item_id, amount, status) "
"VALUES ($1,$2,$3,$4,'pending') ON CONFLICT (idem_key) DO NOTHING "
"RETURNING id", idem_key, user_id, item_id, amount,
)
if existing is None:
return await db.fetchval(
"SELECT status FROM payments WHERE idem_key = $1", idem_key
)
# 2. серверная проверка цены — НЕ доверяем клиенту
real_price = PRICE_TABLE[item_id]
if amount != real_price:
await mark_failed(idem_key, "price_mismatch")
raise FraudError(f"price tampered: got {amount}, expected {real_price}")
# 3. атомарное списание
ok = await db.execute(
"UPDATE balances SET amount = amount - $1 "
"WHERE user_id = $2 AND amount >= $1", amount, user_id,
)
if ok == "UPDATE 0":
await mark_failed(idem_key, "insufficient_funds")
raise InsufficientFundsError()
await grant_item(user_id, item_id)
await mark_success(idem_key)
return "success"
Ключевое: сумма берётся с сервера по item_id, а не из клиентского payload. Idem-ключ — обычно конкатенация user_id, item_id и платёжного идентификатора провайдера. Все отклонения (price_mismatch, double_spend, insufficient_funds) логируются в security-канал и идут на алерт.
Логирование, алерты и security-канал
Всю подозрительную активность пишите в отдельный лог (или Sentry breadcrumbs с тегом security). Это нужно не «на всякий», а чтобы за час понять масштаб инцидента.
Что обязательно логировать:
- срабатывания rate-limit (chat_id, IP, endpoint);
- неудачные CAPTCHA с причиной (timeout, неверный ответ, истёк токен);
- shadow-баны и автоматические муты;
price_mismatchв платежах;- 4xx/5xx на webhook;
risk_score >= 30;- все
secret_tokenmismatch (это уже атака).
Алерты — на резкие отклонения, не на пороги. Полезные правила:
- всплеск
/start> 5σ от 7-дневного среднего за 5 минут; - доля shadow-баненных юзеров за час > 10%;
- аномалия по гео (внезапно 60% трафика из одной страны, которой обычно 2%);
- новый
callback_data, которого нет в кодовой базе (попытка fuzz-инга); - удвоение времени обработки апдейтов (DDoS-индикатор).
В Prometheus метрики складываются в 4–5 строк:
from prometheus_client import Counter, Histogram
started_total = Counter("max_bot_start_total", "starts", ["risk_bucket"])
ratelimit_hits = Counter("max_bot_ratelimit_total", "rl hits", ["scope"])
captcha_fails = Counter("max_bot_captcha_fail_total", "captcha fails", ["reason"])
payment_fraud = Counter("max_bot_payment_fraud_total", "fraud", ["reason"])
secret_mismatch = Counter("max_bot_secret_mismatch_total", "bad secret_token")
WAF и edge-защита
Лучше не пускать мусорный трафик в приложение вообще. На уровне Nginx или CDN включаем:
- лимит запросов per IP (
limit_req, см. выше); - блокировку известных bad-IP списков (Spamhaus, AbuseIPDB);
- challenge для подозрительных user-agent;
- блокировку запросов без
Content-Type: application/jsonна webhook; - rate-limit на
User-Agent(не только на IP), чтобы один IP за NAT не блокировал всех.
Cloudflare Free + WAF rules покрывают 80% случаев бесплатно. Для российского хостинга — Yandex Smart Web Security, который умеет работать в одной зоне с SmartCaptcha.
Чёрный список и shadow-ban
Хранить простой Redis-set с user_id забаненных:
async def is_banned(user_id: int) -> bool:
return bool(await redis.sismember("ban:hard", user_id))
async def is_shadow(user_id: int) -> bool:
return bool(await redis.sismember("ban:shadow", user_id))
async def ban_middleware(handler, event, data):
uid = event.from_user.id
if await is_banned(uid):
return # тихо игнорируем
if await is_shadow(uid):
if hasattr(event, "answer"):
await event.answer("Готово ✅")
return
return await handler(event, data)
Hard-ban — для подтверждённых ботов и фрода. Shadow-ban — для подозрительных, но непонятных случаев, с TTL 7–30 дней. Истёк — снимается автоматически.
Юр-нюансы: оферта и отказ в обслуживании
Бот для платных услуг — это публичная оферта (ст. 437 ГК РФ). Отказ в обслуживании без оснований может быть оспорен пользователем, поэтому в условиях оферты явно пропишите:
- право отказа при подозрении на автоматизированную активность;
- право блокировки за нарушение правил пользования;
- хранение логов и причин блокировки 6+ месяцев;
- порядок обжалования (контактный email).
Для 152-ФЗ: IP, user_id, поведенческие сигналы тоже считаются персональными данными в широкой трактовке РКН — в политике обработки укажите цели и срок хранения. Передачу в SmartCaptcha и WAF тоже описываем (это передача ПДн третьему лицу, нужен соответствующий пункт).
Pen-testing бота: OWASP Top-10 для ботов
Перед запуском прогоните бот по чек-листу:
| Категория OWASP | Проверка для MAX-бота |
|---|---|
| Broken Access Control | Можно ли получить чужой профиль/заказ по ID? |
| Cryptographic Failures | Токен в env, не в git, маска в логах? |
| Injection | SQL/NoSQL/LLM-injection в полях форм? |
| Insecure Design | Есть ли rate-limit на дорогие операции? |
| Security Misconfiguration | secret_token, CORS, IP-allowlist? |
| Vulnerable Components | SDK MAX и зависимости свежие? |
| Auth Failures | initData Mini App проверяется на каждом запросе? |
| Data Integrity | Idempotency на платежах? |
| Logging Failures | Security-лог + алерты есть? |
| SSRF | Бот ходит во внешние URL по запросу юзера? |
Полезные инструменты: pip-audit / npm audit, Semgrep с правилами для Python+Bot, ручной fuzzing callback_data через свой второй бот, нагрузочное тестирование через k6 с реалистичным профилем.
Что не работает
- Чёрный список IP пользователей — для бота это бессмысленно, IP пользователя нам обычно недоступен (разве что в Mini App).
- CAPTCHA на каждом шаге — пользователи разбегутся, конверсия рухнет.
- Бан по
user_idнавсегда — атакующий заведёт новый аккаунт за минуту. - Безусловное доверие данным от пользователя — никогда; всё валидируем на сервере.
- «Безопасность через неизвестность» — webhook URL рано или поздно засветится, на это нельзя закладываться.
Библиотеки и сервисы
Что брать в стек для MAX-бота:
| Задача | Python | Node.js |
|---|---|---|
| Bot framework | официальный SDK MAX, aiohttp | официальный SDK, fastify |
| Rate-limit | redis + Lua, slowapi | rate-limiter-flexible, bottleneck |
| Validation | pydantic v2 | zod |
| WAF/edge | Yandex SWS, Cloudflare | то же |
| CAPTCHA | Yandex SmartCaptcha | то же |
| Secrets | Vault, Yandex Lockbox | то же |
| Monitoring | Sentry, Prometheus, Grafana | то же |
| LLM-safety | guardrails, llm-guard | то же |
Итого
Антибот-защита бота в MAX строится на нескольких слоях, и каждый слой ловит свой класс угроз. CAPTCHA при /start и Yandex SmartCaptcha на критичных формах отсекают накрутку базы и автоматизированный фрод. Rate-limit per user через Redis Lua и per IP через Nginx гасит флуд. secret_token, IP-allowlist и WAF защищают webhook от подделки. Idempotency-ключи и серверная проверка цен закрывают платежи от double-spend. Поведенческий scoring и shadow-ban ловят умных сборщиков, которые проходят CAPTCHA. Security-канал с алертами на отклонения от baseline даёт реакцию в реальном времени, а не «через неделю по жалобам». Юр-обвязка (оферта, политика 152-ФЗ) делает блокировки правомерными. Ни один слой по отдельности не покрывает всё, но вместе они снижают риски на 95–99%. Закладывать защиту имеет смысл с самого начала проекта — переписывать дороже, чем спроектировать сразу, особенно если бот уже принимает деньги или хранит ПДн.
Частые вопросы
Какие основные угрозы для бота в MAX нужно учитывать?
Угрозы делятся на три класса. Технические: накрутка регистраций через массовый /start, scrape базы пользователей через enum-эндпоинты, DDoS на webhook, парсинг каталога. Экономические: abuse платных функций (подмена суммы или ID товара), double-spend через ретраи webhook, фрод-заявки в формах. Репутационные: спам в чатах, где бот админ, фишинг через бота, prompt-injection в LLM-ботах. Отдельно — перебор номеров заказа для чтения чужих ПДн и заваливание формы заявок фейковыми лидами. Защита нужна слоями: для каждой угрозы свой механизм, ни один слой по отдельности не закрывает всё.
Какую CAPTCHA использовать в боте MAX и как не убить конверсию?
Есть три формата: кнопочная (арифметика, выбор страны), текст с картинки и внешняя через Mini App. Минимум — кнопочная CAPTCHA с TTL 60 секунд при /start, реальные пользователи проходят с первого раза. Для критичных сценариев (форма заявки, платежи, промокоды) — Yandex SmartCaptcha как лучший выбор для РФ: интегрируется через Mini App или внешний URL, валидируется на сервере через https://smartcaptcha.yandexcloud.net/validate. CAPTCHA на каждом шаге убьёт UX — ставим только на старте и на критичных. Если бот идёт по входящему трафику с сайта, CAPTCHA можно вынести на посадку до перехода в бот.
Как организовать rate-limit на двух уровнях — пользователь и IP?
Per user: token bucket в Redis через Lua-скрипт (атомарность, нет round-trip). Параметры: capacity 20, refill 2 токена в секунду, дорогие операции стоят 5 токенов. Хранится в ключе rl:user:CHAT_ID. Per IP: ставится на уровне Nginx или edge до бизнес-логики через limit_req_zone с rate 30r/s burst 60. Это убирает мусорный трафик до приложения. Дополнительно — IP-allowlist MAX (когда VK Tech опубликует диапазоны) или WAF Yandex SWS / Cloudflare. Лимиты в Redis переживают рестарт приложения и работают между инстансами при горизонтальном масштабировании.
Как защитить webhook MAX-бота от подделки запросов?
Используйте параметр secret_token при setWebhook. MAX присылает значение в заголовке X-Max-Bot-Api-Secret-Token, всё без него или с неверным значением отбрасываем. Сравнение строго через hmac.compare_digest, не через ==, иначе теоретически возможна timing attack. Параллельно: HTTPS обязателен, IP-allowlist MAX-диапазонов, WAF на уровне edge, rate-limit per IP в Nginx, блокировка запросов без Content-Type: application/json. Replay-атаки гасятся idempotency-ключом в платёжных операциях. Ротация секрета — раз в квартал и при увольнении из команды.
Какие поведенческие сигналы помогают вычислить ботов в MAX?
MAX-специфичные: возраст аккаунта по эвристике user_id (свежие подозрительны), отсутствие фото и заполненного имени, неверифицированный номер телефона, несоответствие language_code и геозоны привязки номера, пустой username. Поведенческие: много /start подряд без интерактива, нулевая доля текстовых сообщений при куче нажатий кнопок, аномально быстрые ответы (меньше 200 мс), регистрация из IP дата-центров (если есть Mini App), хождение строго по золотому пути сценария без ошибок. Простая scoring-модель суммирует баллы: больше 50 — shadow-ban, 30..49 — повторная CAPTCHA, меньше 30 — пропускаем.
Как защитить платные функции от подмены суммы и double-spend?
Главное правило: никогда не доверяйте сумме из клиентского payload. Сумма берётся с сервера по item_id из таблицы цен. Хранение корзины — на сервере, не в Mini App. Идемпотентность: уникальный idem_key (user_id плюс item_id плюс платёжный идентификатор), вставляется через INSERT ON CONFLICT DO NOTHING — если уже есть, возвращаем прошлый статус. Атомарное списание баланса через UPDATE balances SET amount = amount - X WHERE amount >= X — атомарно проверяем достаточность средств. Все отклонения (price_mismatch, double_spend, insufficient_funds) логируются в security-канал и идут на алерт. Возвраты — только через оператора, не автомат.
Какие алерты обязательно настроить для антибот-защиты?
Минимум пять через Prometheus + Grafana или Sentry. Всплеск /start больше 5σ от 7-дневного среднего за 5 минут — индикатор массовой атаки сборщика. Доля shadow-баненных юзеров за час больше 10% — что-то пошло не так со scoring или идёт массовая атака. Аномалия по гео (внезапно 60% трафика из одной страны, которой обычно 2%) — координированная атака. Новый callback_data, которого нет в кодовой базе — попытка fuzz-инга. Удвоение времени обработки апдейтов — DDoS-индикатор. Дополнительно — все secret_token mismatch (это уже атака на webhook). Алерты лучше на отклонение от baseline, а не на статичные пороги.