Защита форм от спама без капчи: ловушка, метка времени и лимиты
Безопасность ♡
нам не сюда ♡
Спамеры находят любую открытую форму за считанные дни, это закон природы. Капча помогает, но выглядит как наказание для нормальных людей: «докажи, что ты не робот, найди все светофоры». Я собрала защиту из нескольких дешёвых приёмов. Каждый по отдельности ничего не решает, а вместе они отсекают почти всё. Показываю, из чего это сделано.
Ловушка для ботов: скрытое поле
Идея простая и слегка коварная: добавляем в форму поле, которое человек не видит, а бот заполнит, потому что боты заполняют всё подряд, как я в магазине корзину.
<div class="hp" aria-hidden="true">
<label for="website">Сайт</label>
<input id="website" name="website" tabindex="-1" autocomplete="off">
</div>
А в правилах валидации требуем, чтобы это поле осталось пустым:
'website' => ['nullable', 'max:0'],
Поле прячется через CSS, а не через "type="hidden"": боты специально пропускают скрытые типы, зато обычное текстовое поле заполняют с удовольствием.
Метка времени: человек так быстро не печатает
Второй приём это зашифрованная метка времени, которая кладётся в форму при загрузке страницы. Бот отправляет форму мгновенно, а человеку нужно хотя бы пара секунд, даже если он очень торопится. Проверка выглядит так:
public static function formToken(): string
{
return Crypt::encryptString((string) now()->timestamp);
}
$age = ($now ?? now()->timestamp) - $issued;
return match (true) {
$age < self::MIN_SECONDS => 'too_fast',
$age > self::MAX_SECONDS => 'expired',
default => 'ok',
};
Минимум у меня 3 секунды, максимум сутки. Шифрование нужно, чтобы бот не подделал подходящее время: расшифровать метку без ключа приложения нельзя.
Ссылки в комментариях и лимиты запросов
Почти весь спам в комментариях это ссылки. Поэтому анонимным комментаторам я вообще запретила ссылки проверкой по регулярному выражению. Да, жёстко, зато тихо:
public static function hasLinks(string $text): bool
{
return (bool) preg_match('~(https?://|www\.|ftp://|\[url|<a\s|\b[\p{L}\d-]+\.(?:com|ru|net|org|info|xyz|top|club|site|online|shop|biz|cc|tk|ml)\b)~iu', $text);
}
Третий слой это лимиты запросов. В Laravel они настраиваются в пару строк, причём ограничивать можно по нескольким ключам сразу:
RateLimiter::for('subscribe', fn (Request $request) => [
Limit::perMinute(3)->by('sub-min|'.$request->ip()),
Limit::perHour(10)->by('sub-hour|'.$request->ip()),
Limit::perHour(3)->by('sub-mail|'.mb_strtolower((string) $request->input('email'))),
]);
Так с одного адреса форму не забросать, а на одну почту нельзя слать бесконечные письма подтверждения. Для комментариев лимит 3 в минуту и 25 в сутки, для входа в админку 5 попыток в минуту по паре «адрес плюс почта» и 10 в час на почту. Подбирать пароль к админке при таких лимитах можно примерно до второго пришествия.
Одинаковый ответ для всех
В форме подписки я всегда отвечаю одинаково, независимо от того, подписан ли уже такой адрес. Иначе по ответу можно проверять, кто у тебя в списке, а это утечка. Мелочь, которая экономит нервы читателям.
Когда всего этого мало
Если спама всё равно много, на помощь приходит Cloudflare Turnstile: невидимая проверка, которая обычно людям не мешает. У меня она подключается через два ключа в ".env" и включается сама, если они заданы. Без ключей форма спокойно работает на остальных приёмах.
Как это выглядит в связке с подпиской на рассылку, я показывала в статье про дыру в подписке, а про защиту от запросов на чужие адреса писала в статье про SSRF.
Комментарии
Буду рада вашим мыслям, идеям и вопросам!
Давайте обсуждать ♡
Делитесь
своими мыслями
— мне очень
это важно ♡