Legan Studio
Все статьи
~ 16 мин чтения

Капчи и антибот защита в боте MAX

Как защитить бот в MAX от спама, парсеров и атак: капчи, rate limiting, фильтрация подозрительной активности, поведенческий анализ.

  • MAX
  • безопасность

Любой публичный бот в 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 внутри бота:

  1. Кнопочная — простая арифметика, выбор «лишнего» элемента, выбор страны.
  2. Текст с картинки — рендерим PNG с искажённым числом, ответ вводится текстом.
  3. Внешняя через 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% автоматических парсеров:

  1. Honey-pot кнопка — невидимая для человека (например, с эмодзи нулевой ширины), но скрипт её нажмёт по индексу. Помечаем как бота.
  2. Подождать 1.5–2 секунды перед отправкой первого ответа — нормальный пользователь не торопится. Скрипт за это время уже пришлёт следующий апдейт, и мы увидим аномалию.
  3. Простой вопрос — «выберите страну», «нажмите красную кнопку».

Это не остановит мотивированного злоумышленника, но сильно поднимает стоимость атаки.

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_token mismatch (это уже атака).

Алерты — на резкие отклонения, не на пороги. Полезные правила:

  • всплеск /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, маска в логах?
InjectionSQL/NoSQL/LLM-injection в полях форм?
Insecure DesignЕсть ли rate-limit на дорогие операции?
Security Misconfigurationsecret_token, CORS, IP-allowlist?
Vulnerable ComponentsSDK MAX и зависимости свежие?
Auth FailuresinitData Mini App проверяется на каждом запросе?
Data IntegrityIdempotency на платежах?
Logging FailuresSecurity-лог + алерты есть?
SSRFБот ходит во внешние URL по запросу юзера?

Полезные инструменты: pip-audit / npm audit, Semgrep с правилами для Python+Bot, ручной fuzzing callback_data через свой второй бот, нагрузочное тестирование через k6 с реалистичным профилем.

Что не работает

  • Чёрный список IP пользователей — для бота это бессмысленно, IP пользователя нам обычно недоступен (разве что в Mini App).
  • CAPTCHA на каждом шаге — пользователи разбегутся, конверсия рухнет.
  • Бан по user_id навсегда — атакующий заведёт новый аккаунт за минуту.
  • Безусловное доверие данным от пользователя — никогда; всё валидируем на сервере.
  • «Безопасность через неизвестность» — webhook URL рано или поздно засветится, на это нельзя закладываться.

Библиотеки и сервисы

Что брать в стек для MAX-бота:

ЗадачаPythonNode.js
Bot frameworkофициальный SDK MAX, aiohttpофициальный SDK, fastify
Rate-limitredis + Lua, slowapirate-limiter-flexible, bottleneck
Validationpydantic v2zod
WAF/edgeYandex SWS, Cloudflareто же
CAPTCHAYandex SmartCaptchaто же
SecretsVault, Yandex Lockboxто же
MonitoringSentry, Prometheus, Grafanaто же
LLM-safetyguardrails, 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, а не на статичные пороги.