Защита форм от спама без капчи: ловушка, метка времени и лимиты

0

Безопасность ♡

Защита форм от спама без капчи: ловушка, метка времени и лимиты
Боты,
нам не сюда ♡

Спамеры находят любую открытую форму за считанные дни, это закон природы. Капча помогает, но выглядит как наказание для нормальных людей: «докажи, что ты не робот, найди все светофоры». Я собрала защиту из нескольких дешёвых приёмов. Каждый по отдельности ничего не решает, а вместе они отсекают почти всё. Показываю, из чего это сделано.

Ловушка для ботов: скрытое поле

Идея простая и слегка коварная: добавляем в форму поле, которое человек не видит, а бот заполнит, потому что боты заполняют всё подряд, как я в магазине корзину.

<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.

Комментарии

Буду рада вашим мыслям, идеям и вопросам!
Давайте обсуждать ♡

Пока никто не написал.
Станьте первым! ♡

Поддержать проект

Угостить кофе по СБП

Перевод через Систему быстрых платежей, сумма любая. Спасибо, что вы здесь ♡

QR-код для перевода по СБП
Или другим способом
Дневник на почте

Подписаться на блог

Когда выйдет новая запись, я пришлю письмо со ссылкой. Без рассылок «просто так» и без спама. Отписаться можно одним нажатием.

Или следить в Telegram