Антиспам форм без капчи: 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.txt → spam_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. По услугам:
- SEO для услуг - техника, schema, и в том числе антиспам форм как часть гигиены
- Независимый аудит подрядчика - проверка текущего стека
Близкое по теме: CLS 0.377 → 0.002 за день и OPcache на shared-хостинге - другие быстрые правки с большим эффектом.
Вопрос-ответ