Антиспам форм без капчи: 6 фильтров, которые ловят 99% мусора

Honeypot, regex на URL, длина телефона, не-кириллица, rate-limit, лог. Шесть фильтров без капчи закрывают почти весь спам форм без боли пользователя.

Капча - это налог на конверсию. По разным замерам она теряет от 3% до 8% реальных заявок, особенно на мобильных. Я с самого начала проекта решил без капчи. Спам режется шестью простыми фильтрами на PHP, каждый из которых закрывает свой класс атак. В сумме - около 60 строк кода, ноль зависимостей и зеро запросов на сторонние сервисы.

Если коротко

  • 6 фильтров: honeypot, regex на URL, длина телефона, не-кириллица, rate-limit, лог.
  • Капчу не использую - теряет 3-8% конверсии и привязывает форму к стороннему сервису.
  • Все фильтры на сервере (никакой JS, никаких зависимостей). Боты, которые ходят без JS-движка, не обходят.
  • Спам падает на 99% в моих метриках, легитимные заявки проходят без потерь.

Фильтр 1: Honeypot

В форму добавляется скрытое поле - обычно с именем типа website или url, на которые боты реагируют активнее всего. Скрывается через CSS (не type="hidden", потому что боты часто игнорируют скрытые поля по type).

<input type="text" name="website" tabindex="-1" autocomplete="off"
       style="position:absolute;left:-9999px;width:0;height:0;visibility:hidden">

На сервере проверяется первым:

if (!empty($_POST['website'])) {
    log_spam('honeypot', $_POST);
    exit; // Молча отбрасываем, без сообщения
}

Важно - отбрасывать молча. Не возвращать «вы бот». Боты учатся: если ваш сервер реагирует на пойманный honeypot редиректом или ошибкой, они начинают понимать, что поле проверяется. Лучше делать вид, что заявка принята: возвращаете 200 OK и обычную страницу «спасибо». Бот думает, что всё прошло, переходит к следующей цели.

В моих логах honeypot ловит 60-80% всех атак на формы. Это самый простой и эффективный фильтр.

Фильтр 2: Regex на URL в комментарии

Большая часть оставшегося спама - это посты с ссылками на казино, реплики, эскорт. Они вставляют URL прямо в поле комментария или сообщения.

$message = $_POST['message'] ?? '';
if (preg_match('#https?://|www\.|\.com/|\.ru/|\.net/|\.shop/#i', $message)) {
    log_spam('url_in_message', $_POST);
    exit;
}

Это блокирует «нормальный» спам с ссылками. Бывает, что легитимный пользователь хочет указать ссылку - например, «вот наш сайт example.com». Решение - не использовать поле «сообщение» как универсальное. У меня в форме отдельное поле «сайт» (опциональное, проходит через свой собственный URL-валидатор), а в message URL по правилам запрещены.

Если ваши клиенты часто упоминают сайты - ослабьте фильтр до явного http:// и https://, а не каждую точку с доменом. Тогда упоминание example.com пройдёт, а реальная ссылка https://casino.xyz режется.

Фильтр 3: Длина телефона

Это про здравый смысл. В российском телефоне минимум 10 цифр (без кода страны) или 11 (с кодом). Боты обычно ставят рандом из 4-7 цифр, или вообще 16-20 цифр (имитируют международный формат с лишним мусором).

$phone = preg_replace('/\D/', '', $_POST['phone'] ?? '');
$len = strlen($phone);
if ($len < 10 || $len > 11) {
    log_spam('phone_length', $_POST);
    exit;
}

preg_replace('/\D/', '') удаляет всё кроме цифр - пробелы, дефисы, скобки. После этого считаете длину. Россия и СНГ почти всегда 10-11 цифр. Если у вас международные клиенты - расширяете до 15 (E.164 максимум).

Дополнительно можно проверить, что номер начинается с 7 или 8 - для пострадавших от подмены первой цифры:

if ($len === 11 && !in_array($phone[0], ['7', '8'])) {
    log_spam('phone_country', $_POST);
    exit;
}

Фильтр 4: Не-кириллица в комментариях

Сайт русскоязычный, реальные клиенты пишут по-русски. Боты с английским спамом фильтруются по соотношению кириллических символов к общему числу.

$message = $_POST['message'] ?? '';
$len = mb_strlen($message);
if ($len > 5) {
    // Считаем кириллические символы
    preg_match_all('/[\p{Cyrillic}]/u', $message, $matches);
    $cyr = count($matches[0]);
    if ($cyr / $len < 0.3) {
        log_spam('not_cyrillic', $_POST);
        exit;
    }
}

30% кириллицы - рабочий порог. Двуязычный комментарий вида «уборка квартиры, area 80 m², 2 bathrooms» проходит. Чистый английский Spam - режется. Чистый русский, естественно, проходит.

Для англоязычных сайтов фильтр инвертируется - проверяете соотношение латиницы. Для двуязычных можно либо отключить, либо повысить порог до 80% - но тогда пропустите больше спама.

Фильтр 5: Rate-limit 5 запросов в час на IP

Один пользователь не отправляет десять заявок в минуту. Если IP делает больше 5 запросов за час - это атакующий, который тестирует фильтры или льёт массовый спам.

Без Redis-кластера на shared-хостинге проще всего хранить в файле или MySQL. У меня в MySQL:

CREATE TABLE rate_limit (
    ip VARCHAR(45) NOT NULL,
    ts INT UNSIGNED NOT NULL,
    KEY idx_ip_ts (ip, ts)
);

Перед обработкой заявки:

$ip = $_SERVER['REMOTE_ADDR'];
$hour_ago = time() - 3600;

$pdo->prepare("DELETE FROM rate_limit WHERE ts < ?")->execute([$hour_ago]);

$stmt = $pdo->prepare("SELECT COUNT(*) FROM rate_limit WHERE ip = ? AND ts > ?");
$stmt->execute([$ip, $hour_ago]);
$count = $stmt->fetchColumn();

if ($count >= 5) {
    log_spam('rate_limit', $_POST);
    exit;
}

$pdo->prepare("INSERT INTO rate_limit (ip, ts) VALUES (?, ?)")->execute([$ip, time()]);

Чистка таблицы - каждые сутки через cron-job, чтобы не разрасталась.

Подводный камень - пользователи за NAT (корпоративная сеть, мобильный оператор). Если 10 человек из одного офиса заполняют формы - рейт-лимит на офисный IP сработает. Лимит 5 в час обычно хватает для этого: реальный пользователь не отправляет больше 1-2 заявок, у вас остаётся запас. Если опасаетесь - поднимаете до 10-20 в час. Главное - не убирайте совсем, иначе массовая атака с одного IP положит почту.

Фильтр 6: Лог в защищённый файл

Все отбитые заявки записываются в spam_log.txt. Не в общий лог сервера, не в БД (БД дороже на write), а в простой текстовый файл:

function log_spam(string $reason, array $data): void {
    $entry = date('c') . ' | ' . $reason . ' | IP ' . $_SERVER['REMOTE_ADDR'] . ' | ' . json_encode($data, JSON_UNESCAPED_UNICODE) . "\n";
    file_put_contents(__DIR__ . '/../spam_log.txt', $entry, FILE_APPEND | LOCK_EX);
}

Лог обязательно защищается на уровне веб-сервера - иначе кто угодно скачает его и увидит ваши спам-паттерны. Для Apache в .htaccess корня:

<Files "spam_log.txt">
    Require all denied
</Files>

Для Nginx - в конфиге локации:

location = /spam_log.txt {
    deny all;
}

Что лог даёт. Раз в неделю я открываю его и смотрю. Видно, какие фильтры срабатывают чаще: если 90% - honeypot, остальные практически не работают, потому что боты до них не доходят. Видно паттерны атак: внезапная атака с одного IP-диапазона, поток китайских комментариев со ссылками на одну и ту же тему. По этим паттернам можно докрутить фильтры или временно забанить диапазон в .htaccess.

Через 30 дней я ротирую spam_log.txtspam_log.txt.bak, новый пустой создаётся автоматически. Старый держу один период для разбора, потом удаляю.

Порядок фильтров

Важная вещь - проверять в правильном порядке, от дешёвого к дорогому. Чтобы не делать SQL-запрос rate-limit, если honeypot уже пометил заявку как мусор.

// 1. Honeypot - самый дешёвый
if (!empty($_POST['website'])) { log_spam('honeypot', $_POST); exit; }

// 2. Regex на URL - тоже дёшево (in-memory)
if (preg_match('#https?://#i', $_POST['message'] ?? '')) { ... }

// 3. Длина телефона - дёшево
if (strlen(preg_replace('/\D/', '', $_POST['phone'] ?? '')) < 10) { ... }

// 4. Не-кириллица - чуть дороже из-за регекспа на utf-8
preg_match_all('/[\p{Cyrillic}]/u', $_POST['message'] ?? '', $m);
// ...

// 5. Rate-limit - самый дорогой, требует SQL
$pdo->prepare("SELECT COUNT(*) FROM rate_limit WHERE ...");

Это даёт минимальную нагрузку - большая часть атак отсекается на первых двух фильтрах, до того, как сервер коснётся БД.

Что не закрывают эти фильтры

Шесть фильтров режут автоматический спам. Они не закрывают:

  • Целенаправленные атаки от человека. Если кто-то сидит и руками заполняет вашу форму с конкурирующими предложениями - фильтры пропустят. Но это редкая и дорогая для атакующего история. Один-два случая в год - на разбор уходит 5 минут.
  • Лидогенерационный спам через CRM. Бывает, что подрядчик «лидогенератор» массово регистрирует на сайтах знакомых нашего клиента подставными данными. С honeypot не сработает (человек заполняет руками), но rate-limit поймает.
  • DDoS на POST-эндпоинт. Это уровень сервера, не приложения. Защищается через nginx limit_req или CDN. Beget CDN это делает на edge-уровне бесплатно.

В сумме фильтры покрывают 99% автоматического трафика, который атакует обычный B2B-сайт услуг. Это достаточно для бизнеса, который не является целью таргетированной атаки.

Целиком кейс по запуску самописного сайта на shared с PHP 8.4 - в статье «50 дней SEO в B2B-клининге». Антиспам форм - часть первой недели работ.

Если у вас сайт с проблемами форм или нужна точная настройка защиты - пишите в Telegram @dimik90. По услугам:

Близкое по теме: CLS 0.377 → 0.002 за день и OPcache на shared-хостинге - другие быстрые правки с большим эффектом.

Вопрос-ответ

Частые вопросы

Почему я против reCAPTCHA и других готовых решений?
Три причины. Первая - конверсия. По разным исследованиям reCAPTCHA v2 теряет 3-8% пользователей, особенно на мобильных и со старых устройств. Это деньги. Вторая - внешняя зависимость. reCAPTCHA - это запрос на google.com, который из РФ блокируется и режется DPI. Если человек на корпоративном VPN или у его провайдера фильтр - форма просто не отправляется, без понятной ошибки. Третья - приватность. reCAPTCHA собирает фингерпринт устройства и отправляет в Google, это лишний риск по 152-ФЗ для российского сайта.
Что такое honeypot и как он работает?
Это поле в форме, скрытое от пользователя через CSS, но видимое для парсеров и ботов. Боты обычно заполняют все поля подряд - text inputs, urls, emails - потому что не умеют различать видимые и скрытые. Если поле заполнено - это бот, заявку отбрасываем. Реальные пользователи поле не видят и не заполняют. Простой honeypot закрывает 60-80% автоматического спама без какой-либо обратной связи с пользователем.
Не блокирует ли rate-limit реальных клиентов?
При правильных порогах - нет. У меня стоит 5 заявок в час с одного IP. Реальный сценарий: человек отправил заявку, ошибся в номере телефона, исправил, отправил повторно. Это 2 заявки. Может быть третий раз - добавил уточнение в комментарий. Пятая заявка с одного IP за час - это уже подозрительно. Если у вас офис с NAT на несколько человек и они правда отправляют формы по 10 раз - поднимаете лимит до 20 в час.
Зачем фильтр на не-кириллицу в комментариях?
Сайт русскоязычный, аудитория - Россия и СНГ. Реальные клиенты пишут по-русски. Спам-боты часто публикуют посты на английском или китайском с ссылками на казино, фарму и порно. Фильтр работает так: если в комментарии больше 5 символов и при этом меньше 30% кириллицы - отбрасываем. Это не блокирует двуязычные комментарии (типа «нужна уборка квартиры, area 80 m²»), но режет чистый английский спам. Для англоязычных сайтов фильтр инвертируете.
Где хранить лог спама и зачем его смотреть?
В отдельный файл `spam_log.txt`, защищённый через `.htaccess`. Зачем смотреть. Первое - оценить эффективность фильтров: сколько и каких атак приходит. Второе - обнаружить ложные срабатывания: легитимная заявка попала в лог, нужно поправить фильтр. Третье - словарь нежелательных слов. Постфактум вы видите паттерны (типа постоянных упоминаний «crypto», «replica», «escort») и можете добавить их в blacklist. Лог не должен публиковаться - в `.htaccess` правило `<Files spam_log.txt> Require all denied </Files>`.

Оцените статью

Дальше

← Все записи