Форма обратной связи без спама и CAPTCHA: 6 способов защиты
Как остановить спам в форме без CAPTCHA: ловушка для ботов, временной билет, лимиты и валидация. Код PHP для копирования и ошибка, из-за которой форма рассылает спам.
Форма обратной связи — это самый простой путь клиента к Вам и одновременно любимая цель ботов. Спам в почтовом ящике ещё полбеды. Хуже, когда плохо написанная форма сама начинает рассылать спам с Вашего домена, и Ваши письма потом оказываются у клиентов в папках «Спам».
Самый частый ответ на спам — CAPTCHA: отметьте картинки с гидрантами. Она работает, но имеет свою цену. Часть людей отказывается от отправки сообщения, а reCAPTCHA передаёт данные посетителей Google. В этой статье я показываю 6 способов защиты, которых клиент вообще не заметит. Именно так работает форма на моём сайте и в проектах, которые я делаю для клиентов.
1. Ловушка для ботов (honeypot)
Боты заполняют все поля, которые находят в коде. Поэтому Вы добавляете поле, которого человек не видит. Если оно приходит заполненным, его отправил бот:
<div class="hp" aria-hidden="true">
<label for="www">Оставьте это поле пустым</label>
<input id="www" name="website" type="text" tabindex="-1" autocomplete="off">
</div>
.hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
if (trim($_POST['website'] ?? '') !== '') {
// делаем вид, что всё прошло успешно: бот не узнает, что попал в ловушку, и не будет пробовать дальше
echo json_encode(['ok' => true]);
exit;
}
Три детали решают дело. Поле называется обыкновенно («website»), а не «honeypot». tabindex="-1" не даёт человеку, который перемещается по странице с клавиатуры, попасть в него. aria-hidden скрывает его от программ экранного доступа. Не используйте display: none, потому что часть ботов пропускает такие поля.
2. Временной билет
Человеку нужно несколько десятков секунд, чтобы заполнить форму. Бот отправляет её за доли секунды, часто даже не загружая страницу. Поэтому при загрузке формы Вы выдаёте билет со временем и подписью, а при отправке проверяете, настоящий ли он и сколько ему секунд:
const SEKRET = 'длинная случайная строка, например bin2hex(random_bytes(32)), хранится вне кода';
function bilet_nowy(): string
{
$t = (string) time();
return $t . '.' . hash_hmac('sha256', $t, SEKRET);
}
function bilet_ok(string $bilet): bool
{
if (!preg_match('/^(\d{10})\.([a-f0-9]{64})$/', $bilet, $m)) return false;
if (!hash_equals(hash_hmac('sha256', $m[1], SEKRET), $m[2])) return false; // подделан
$wiek = time() - (int) $m[1];
return $wiek >= 3 && $wiek <= 7200; // не быстрее чем через 3 с и не позже чем через 2 ч
}
Билет можно вставить в форму как скрытое поле. Если страница статична или хранится в кэше, получайте его скриптом с отдельного адреса, когда кто-то начинает заполнять форму. Дополнительный плюс: бот без JavaScript его вообще не получит. Подпись HMAC делает так, что бота нельзя научить подставлять «время минутной давности», потому что без секрета он не сгенерирует корректную подпись.
3. Лимиты обращений
Даже у хитрого бота время ограничено. Лимит на одно соединение, например 5 сообщений за 10 минут, и общий суточный лимит защищают ящик от потока сообщений:
function limit_ok(string $ip, int $max = 5, int $okno = 600): bool
{
// хэш IP вместо самого адреса: Вы не храните персональные данные
$plik = sys_get_temp_dir() . '/formularz-' . hash('sha256', $ip . SEKRET) . '.json';
$fh = fopen($plik, 'c+');
flock($fh, LOCK_EX); // два обращения одновременно не перезапишут друг другу счётчик
$teraz = time();
$czasy = array_filter(json_decode(stream_get_contents($fh), true) ?: [], fn($t) => $t > $teraz - $okno);
$ok = count($czasy) < $max;
if ($ok) $czasy[] = $teraz;
ftruncate($fh, 0);
rewind($fh);
fwrite($fh, json_encode(array_values($czasy)));
flock($fh, LOCK_UN);
fclose($fh);
return $ok;
}
if (!limit_ok($_SERVER['REMOTE_ADDR'] ?? '')) {
http_response_code(429);
exit;
}
Если сайт стоит за промежуточным сервером (Cloudflare, Caddy, Nginx), REMOTE_ADDR — это адрес этого сервера, а не клиента. Настоящий адрес тогда нужно брать из заголовка, который устанавливает Ваш сервер, но только если Вы доверяете тому, кто его устанавливает. Иначе бот впишет любой адрес и обойдёт лимит.
4. Валидация e-mail: ловушка, из-за которой форма рассылает спам
Это самый важный пункт в статье. Типичная форма на PHP отправляет сообщение Вам и устанавливает заголовок Reply-To на адрес клиента, чтобы можно было ответить одним щелчком. Адрес обычно проверяют так:
filter_var($email, FILTER_VALIDATE_EMAIL)
Проблема в том, что по стандарту в адресе e-mail часть до @ может быть заключена в кавычки, а внутри них допускаются символы с обратной косой чертой перед ними, в том числе символ новой строки. Я проверил это в PHP 8.3 и 8.4: адрес в кавычках с обратной косой чертой и переводом строки внутри проходит эту валидацию. Будучи вставленным в заголовок Reply-To, он дописывает новый заголовок, например Bcc: со списком чужих адресов. Тогда Ваша форма рассылает спам с Вашего домена через Ваш почтовый сервер.
Решение простое. Для формы достаточно обычных адресов, поэтому отклоняйте кавычки, косые черты и управляющие символы:
function email_ok(string $e): bool
{
return strlen($e) <= 120
&& filter_var($e, FILTER_VALIDATE_EMAIL) !== false
&& !preg_match('/[\x00-\x20\x7F"\\\\(),:;<>\[\]]/', $e);
}
Второе правило: в заголовки письма не вставляйте ничего, что пришло от пользователя, кроме адреса, проверенного таким образом. Тему и имя лучше кодировать (=?UTF-8?B?...?=) или использовать библиотеку для отправки почты, например PHPMailer или Symfony Mailer, которые сами следят за заголовками. Эту ошибку я нашёл и исправил во время аудита безопасности собственного сайта, поэтому знаю, что она касается реального кода, а не только учебников.
5. Содержание и домен отправителя
У спама есть свои привычки. Несколько простых правил отсеивают многое из того, что пройдёт сквозь ловушки:
$linki = static fn(string $s): int => preg_match_all('~https?://|www\.~i', $s);
if ($linki($imie) > 0 || $linki($tresc) > 3) {
odrzuc('linki'); // ссылка в имени или больше 3 ссылок в тексте
}
// домен адреса e-mail должен принимать почту (запись MX или A)
$domena = substr(strrchr($email, '@') ?: '', 1);
if ($domena === '' || (!checkdnsrr($domena, 'MX') && !checkdnsrr($domena, 'A'))) {
odrzuc('domena');
}
У проверки домена есть ещё одно достоинство: она ловит опечатки настоящих клиентов, например gmial.com. Вместо молчаливого отклонения попросите исправить адрес, иначе Ваш ответ никогда не дойдёт.
6. Вычислительная задача для затратных форм
Если отправка формы что-то стоит, например запускает ИИ, платный API или большой отчёт, стоит добавить вычислительную задачу (proof-of-work). Браузер в фоне ищет число, которое вместе с полученной строкой даёт хэш SHA-256, начинающийся примерно с десятка нулевых битов. Для человека это доли секунды в фоновом режиме, а боту, который хочет отправить тысячу обращений, придётся заплатить тысячекратным процессорным временем. Так я защищаю инструменты на своём сайте, такие как Проверить сайт и аудит конкурентов. Для обычной формы обратной связи это, как правило, не нужно.
Чего не делать
- Не сообщайте боту, почему он отклонён. Ответ «поле website должно быть пустым» — это инструкция по обходу ловушки.
- Не сохраняйте содержание отклонённых сообщений. Достаточно счётчиков: сколько отклонений и по какой причине. Меньше данных — меньше обязанностей по GDPR (RODO).
- Не блокируйте страны и целые сети вслепую. Клиент в отпуске или в фирме с VPN тоже хочет написать.
- Не забывайте о сообщениях для людей. Когда лимит сработает на настоящем клиенте, покажите номер телефона или адрес e-mail, а не один только код ошибки.
Когда всё-таки CAPTCHA?
Если несмотря на эти защиты спам продолжает приходить, это обычно означает человека или бота, управляющего настоящим браузером. Тогда CAPTCHA имеет смысл. Выберите решение, незаметное для большинства людей и ориентированное на приватность, опишите его в политике конфиденциальности и включайте только там, где проблема действительно есть.
Итог
Ловушка для ботов, временной билет и лимиты останавливают автоматический спам без всякой CAPTCHA. Правильная валидация e-mail не даст форме превратиться в рассылку спама с Вашего домена. Если письма Вашей фирмы и так попадают у клиентов в спам, проверьте также настройки домена, о которых я пишу в статье SPF, DKIM и DMARC шаг за шагом.
Хотите, чтобы я проверил формы и безопасность Вашего сайта? Закажите аудит безопасности или напишите мне. Отвечаю в течение 24 часов, а новые клиенты получают скидку 20% на первый заказ.
Частые вопросы
Она остановит самых простых ботов, которые заполняют все поля. Более хитрые её обходят, поэтому стоит добавить временной билет и лимиты. Вместе эти три уровня останавливают подавляющую часть автоматического спама без всякой CAPTCHA.
reCAPTCHA передаёт Google данные о посетителе, поэтому её нужно описать в политике конфиденциальности, а её правовое основание в ЕС вызывает сомнения. Если CAPTCHA действительно необходима, рассмотрите решения, ориентированные на приватность, такие как Cloudflare Turnstile или Friendly Captcha, и тоже опишите их в политике.
Человека не остановит никакая ловушка. Помогают лимиты обращений с одного соединения, отклонение сообщений с множеством ссылок и проверка того, принимает ли домен адреса e-mail вообще почту. Остальное Вы просто удаляете, но его немного.
Логика та же. В популярных плагинах форм есть ловушки для ботов и лимиты, часто в дополнениях. Только проверьте, не отправляет ли антиспам-плагин содержание сообщений во внешний сервис, потому что тогда это тоже нужно описать в политике конфиденциальности.
Связанные услуги
Есть похожий проект?
Опишите, что Вам нужно, и в течение 24 часов я пришлю оценку. Новые клиенты получают скидку 20% на первый заказ.


