Защита сайта от ботов без упора на CAPTCHA: серверная валидация, rate limiting и step-up проверки
Если на сайте до сих пор вся защита от ботов сводится к одной CAPTCHA перед формой, проблема уже не в ботах, а в архитектуре. В 2026 году бот умеет открыть нормальный браузер, дождаться рендера, пройти часть клиентской логики, поменять IP и, если нужно, отдать challenge на solver-сервис. Поэтому инженерный вопрос звучит не так: "какую CAPTCHA поставить?", а так: "какие сигналы мы проверяем на сервере и что делаем, если запрос выглядит подозрительно".
У той же Cloudflare Turnstile серверная проверка обязательна: клиентский виджет сам по себе ничего не защищает. В документации прямо сказано, что токены можно подделать, они живут только 300 секунд и принимаются один раз. Если сервер не вызывает Siteverify, злоумышленник может отправить на ваш endpoint любую строку вместо токена. Это типичная ошибка интеграции.
Где именно ломается схема "поставили CAPTCHA и успокоились"
CAPTCHA обычно ставят в одной точке: логин, регистрация, форма обратной связи или checkout. Но атака редко живет в одной точке.
Вот что бот умеет на практике:
- автоматизировать браузер через Playwright, Puppeteer или Selenium;
- распределять запросы по residential proxy и ASN, чтобы не упираться в один IP;
- отправлять challenge на внешний solver;
- переиспользовать нормальный пользовательский поток, если backend проверяет только факт наличия токена, а не его валидность;
- размазывать попытки по времени, чтобы не упереться в простой лимит
N запросов в минуту.
Поэтому OWASP выделяет не один "бот-тип", а целый набор автоматизированных угроз. В реестре OWASP Automated Threats отдельно перечислены OAT-009 CAPTCHA Defeat и OAT-008 Credential Stuffing, который определяется как массовые логин-попытки для проверки украденных пар логин/пароль.
Из этого следует простой инженерный вывод: CAPTCHA можно оставить как один из сигналов, но нельзя делать её центром защиты.
Что должно происходить на сервере после challenge
Технически корректная схема выглядит так:
- Клиент получает токен challenge.
- Клиент отправляет токен вместе с действием: логин, регистрация, лид-форма, checkout.
- Backend валидирует токен на стороне сервера.
- Backend сверяет, что токен не просрочен, не переиспользован и относится к ожидаемому действию.
- Backend объединяет результат challenge с другими сигналами: IP, заголовки, частота попыток, fingerprint, репутация ASN, качество сессии.
- Только после этого сервер решает, что делать: пропустить запрос, попросить step-up, заморозить действие или отдать запрос на ручную проверку.
Cloudflare Turnstile в документации по серверной валидации отдельно пишет три вещи, которые важны в коде:
- токен можно подделать, если вы не валидируете его на backend;
- токен живет
5минут; - токен одноразовый, и повторное использование должно падать с
timeout-or-duplicate.
Это важно не как абстрактная рекомендация, а как конкретное правило интеграции. Если ваш endpoint принимает запрос только потому, что поле cf-turnstile-response вообще пришло, бот уже обошел защиту.
Минимальная реализация: как валидировать challenge правильно
Для формы, логина или регистрации минимальный серверный поток должен быть таким:
const response = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
secret: process.env.TURNSTILE_SECRET,
response: token,
remoteip: clientIp
})
}
);
const result = await response.json();
if (!result.success) {
return rejectRequest('challenge_failed', result['error-codes']);
}Дальше полезно добавить ещё три проверки:
- совпадает ли ожидаемое действие с тем, что вы ждете на endpoint;
- совпадает ли hostname;
- не пришел ли один и тот же токен дважды.
В рекомендациях Google reCAPTCHA по automated threats та же логика сформулирована через action и expectedAction: действие на странице и ожидаемое действие при серверной проверке должны совпадать. Если не совпадают, запрос нельзя считать нормальным.
Именно здесь много команд ошибаются. Challenge ставят на фронте, а backend не связывает токен с реальным действием. В итоге токен с регистрации можно попытаться подсунуть в другой сценарий.
Почему checkbox CAPTCHA ухудшает продукт и почти не помогает архитектуре
Если нужна живая, а не маркетинговая причина, она очень простая: checkbox challenge сам по себе добавляет трение, но не даёт достаточно контекста.
Google в best practices по automated threats прямо пишет, что checkbox keys увеличивают friction и могут бить по conversion rate. Там же в оптимальной схеме Google рекомендует не просто challenge, а score-based keys, action binding и отдельные сценарии для логина, checkout, account creation и других действий.
То есть даже поставщик CAPTCHA уже предлагает мыслить не виджетом, а риск-моделью:
- для логина нужен один action;
- для регистрации другой;
- для checkout третий;
- для account change четвертый.
Одинаковый challenge на все сценарии обычно означает, что защита поставлена "для галочки".
Что добавить кроме CAPTCHA: базовый anti-bot стек для веб-приложения
Ниже стек, который реально полезен разработке.
1. Action-specific rate limiting
Лимиты должны жить не на уровне "весь сайт", а на уровне конкретного действия.
Например:
/login— лимит по IP, login identifier и устройству;/register— лимит по IP, email domain и fingerprint;/contact— лимит по IP, user-agent и частоте похожих payload;/checkout— лимит по аккаунту, корзине, платежному методу и IP.
Самая частая ошибка здесь — лимит только по IP. Для credential stuffing этого мало.
2. Step-up вместо блокировки всех подряд
Не каждый риск должен заканчиваться блоком.
Нормальный flow такой:
- низкий риск — пропускаем без лишнего трения;
- средний риск — включаем challenge или email verification;
- высокий риск — просим MFA, замораживаем действие или режем сессию.
OWASP MFA Cheat Sheet прямо говорит, что MFA — лучшая защита от большинства password attacks, включая credential stuffing и password spraying.
3. Ограничение привилегий после регистрации
Даже если бот зарегистрировал аккаунт, это не значит, что он сразу должен получить полный доступ.
Хорошая практика:
- задержка на публикацию контента;
- лимиты на первые N сообщений или заявок;
- ограничение на массовые API-вызовы;
- отдельная модерация первых действий;
- верификация email или телефона до получения чувствительных прав.
Это особенно полезно для форм, личных кабинетов, маркетплейсов и B2B-сервисов.
4. Honeypot и серверная валидация payload
Для лид-форм и простых контактных сценариев часто помогает не "сильнее challenge", а более умный backend:
- скрытое поле honeypot;
- проверка слишком быстрой отправки формы;
- минимальное время заполнения;
- отказ, если payload повторяется слишком часто;
- нормализация телефона, email и URL до записи в CRM.
Это дешево внедряется и хорошо режет мусорный автоматизированный трафик.
5. Наблюдаемость, а не только блокировка
Если вы не пишете события, вы не защищаете систему, а просто надеетесь.
Минимум, который стоит логировать:
{
"route": "/login",
"action": "account_login",
"ip": "client-ip",
"asn": "asn-id",
"fingerprint": "device-hash",
"challenge_success": true,
"challenge_errors": [],
"rate_limit_bucket": "login:ip+email",
"decision": "step_up",
"latency_ms": 184
}Это нужно, чтобы потом ответить на нормальные инженерные вопросы:
- challenge вообще помогает или только мешает;
- токены чаще падают из-за атак или из-за кривой интеграции;
- какие маршруты атакуют сильнее;
- где больше false positive;
- какие сценарии ломают конверсию.
Cloudflare в аналитике Turnstile советует смотреть хотя бы три базовые метрики: Siteverify requests, Valid tokens и Invalid tokens. Большая доля invalid tokens может означать и ботов, и сломанную интеграцию.
Типичные ошибки интеграции, которые ломают защиту
Вот список, который чаще всего встречается в веб-проектах:
Challenge проверяется только на клиенте
Это самая грубая ошибка. Если backend не вызывает Siteverify, защита фиктивна.
Один и тот же challenge на все действия
Логин, регистрация и checkout — это разные рисковые сценарии. Их нельзя сваливать в один action и одну политику.
Нет проверки expectedAction
Если action с фронта не сверяется на сервере, challenge можно использовать не там, где он был выдан.
Нет отдельной стратегии для credential stuffing
OWASP описывает credential stuffing как массовую проверку украденных пар логин/пароль. Для такого сценария CAPTCHA без MFA и rate limit обычно только тормозит атаку, но не останавливает её.
Нет защиты от replay
Токен challenge нельзя принимать больше одного раза. Это не "дополнительная опция", а часть threat model.
Нет fallback-логики при отказе внешнего провайдера
Если у Cloudflare или другого провайдера challenge временная проблема, у backend должно быть понятное поведение:
- fail closed для high-risk операций;
- step-up или retry для среднего риска;
- деградация без полной поломки UX для low-risk сценариев.
Практический anti-bot план на 2 недели
Если у вас сейчас только CAPTCHA, а нормального backend-контроля нет, разумный план выглядит так:
Неделя 1
- Разделить маршруты по риску:
login,register,contact,checkout,password-reset. - Включить серверную валидацию challenge для каждого чувствительного маршрута.
- Добавить
actionи серверную проверку ожидаемого действия. - Ввести rate limiting не только по IP, но и по email/login/fingerprint.
- Завести логи по
challenge_success,error-codes,decision,latency.
Неделя 2
- Включить step-up для среднего риска.
- Для логина добавить MFA или хотя бы подготовить rollout на high-risk логины.
- Для регистрации и форм добавить honeypot и delayed privileges.
- Сверить метрики: conversion, invalid tokens, false positive, spam rate.
- Прогнать ручной abuse-тест: replay token, подмена action, быстрая отправка формы, массовые попытки логина.
Если нужен не отдельный виджет, а именно серверная логика, контроль маршрутов и разбор аномалий по событиям, это уже задача на веб-разработку и прикладную anti-abuse архитектуру.
