Перейти к содержимому

Спам через формы сайта - капча, скрытое поле, ограничение частоты

Форма обратной связи собирает десятки заявок от ботов в сутки. Разбираем защиту целиком: капча, скрытое поле, ограничение частоты и проверка на сервере.

Механика

Спам приходит двумя разными путями, и защита от них тоже разная. Массовый бот отправляет запрос прямо на адрес обработчика, не открывая страницу. Ручной спам приходит от живого человека через обычную форму на сайте.

Отсюда главное правило защиты: любая проверка заявки живёт на стороне сервера. Скрипт бота не выполняет, разметку не читает и проверку в браузере просто не видит. Проверка на стороне страницы остаётся удобством для посетителя, а не защитой.

Штатная капча ядра работает в два приёма и требует внимания в обоих. При выводе формы код получает идентификатор картинки, а само изображение рисует отдельный служебный скрипт по этому идентификатору. Проверяется введённое слово потом, на сервере, по паре «слово и идентификатор».

Идентификатор одноразовый. После проверки он гасится, поэтому повторная отправка той же формы требует новой картинки, а перерисовать её нужно и при любой другой ошибке заполнения.

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

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

Шаблон формы обязан уметь рисовать поле кода и саму картинку. Иначе после нескольких неудачных попыток входа система потребует код, поля для него в шаблоне не окажется, и человек заблокирует себя сам.

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

Ограничение частоты отправок отсекает массовый поток заявок с одного источника. Одна и та же сессия или один и тот же адрес не должны присылать десять заявок за минуту, и такой поток останавливают до записи в базу.

Проактивный фильтр платформы к защите от спама отношения не имеет. Он ловит попытки внедрения кода в параметры запроса, а обычная заявка от бота выглядит для него безупречно.

Шаги

  1. Включить капчу на публичной форме и проверить наличие библиотек графики.
  2. Убедиться, что шаблон формы рисует и поле ввода, и саму картинку кода.
  3. Проверять введённый код на сервере, до сохранения и до отправки письма.
  4. Добавить скрытое поле-ловушку и отклонять заполненные им заявки.
  5. Ограничить частоту отправок с одной сессии или с одного адреса.
  6. Собрать отсев в одном обработчике события и вести журнал отклонений.

Код

Выводим капчу в шаблоне своей формы:

$code = $APPLICATION->CaptchaGetCode(); // идентификатор одноразовый
?>
<input type="hidden" name="captcha_sid" value="<?= $code ?>">
<!-- картинку рисует служебный скрипт платформы, свой обработчик не нужен -->
<img src="/bitrix/tools/captcha.php?captcha_sid=<?= $code ?>" alt="код с картинки">
<input type="text" name="captcha_word" autocomplete="off" required>

Картинку рисует служебный скрипт платформы по переданному идентификатору. Свой код изображение не готовит и в файл не сохраняет, поэтому подменять этот адрес на собственный обработчик не нужно.

Проверяем код на сервере:

global $APPLICATION;
if (!$APPLICATION->CaptchaCheckCode(
(string)$request->getPost('captcha_word'),
(string)$request->getPost('captcha_sid'))) {
$errors[] = 'Неверный код с картинки'; // и обязательно рисуем новую картинку
$code = $APPLICATION->CaptchaGetCode(); // старый идентификатор уже погашен
}
// проверка только в браузере не защищает: бот шлёт запрос мимо разметки

Проверка возвращает признак совпадения и гасит идентификатор. Ветка ошибки обязана вернуть форму с новой картинкой, иначе посетитель застрянет на странице с уже погашенным кодом.

Добавляем скрытое поле-ловушку:

// в разметке формы: поле спрятано стилями, а не атрибутом hidden
?>
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input type="text" name="company_extra" tabindex="-1" autocomplete="off">
</div>

Ловушку прячут стилями и убирают из порядка обхода клавиатурой. Атрибут скрытого поля здесь хуже: часть ботов такие поля пропускает намеренно, а часть читающих программ объявляет их пользователю.

Отклоняем заполненную ловушку и частые отправки:

$session = \Bitrix\Main\Application::getInstance()->getSession();
$last = (int)($session['feedback_last'] ?? 0);
if ($request->getPost('company_extra') !== '') {
return false; // ловушка заполнена, это бот
}
if (time() - $last < 30) {
$errors[] = 'Слишком частая отправка, подождите немного';
}
$session['feedback_last'] = time(); // отметку ставим после успешной проверки

Сессия годится для мягкого ограничения одного посетителя. Массовую отправку с одного адреса считают в своей таблице или в кэше: сессия у каждого запроса бота своя, и на такой поток она не действует.

Собираем отсев в обработчике события веб-формы:

/local/php_interface/init.php
AddEventHandler('form', 'onBeforeResultAdd',
static function ($formId, &$fields) {
if ($formId != 1) { return true; }
$request = \Bitrix\Main\Context::getCurrent()->getRequest();
if ($request->getPost('company_extra') !== '') {
\Bitrix\Main\Diag\Debug::writeToFile(
$request->getRemoteAddress(), 'ловушка', 'spam.log');
return false; // результат не сохраняется вовсе
}
return true;
}
);

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

Считаем отправки с одного адреса за минуту:

use Bitrix\Main\Type\DateTime;
$ip = \Bitrix\Main\Context::getCurrent()->getRequest()->getRemoteAddress();
$count = FormAttemptTable::getCount([
'=IP' => $ip, // адрес берут у объекта запроса
'>=CREATED_AT' => DateTime::createFromTimestamp(time() - 60),
]);
if ($count >= 5) { return false; } // пять заявок в минуту с адреса
FormAttemptTable::add(['IP' => $ip, 'CREATED_AT' => new DateTime()]);
// старые записи чистит агент: таблица растёт быстрее любой другой

Своя таблица переживает смену сессии и считает весь поток целиком. Адрес берут у объекта запроса, а не из массива окружения: за прокси там окажется адрес посредника, одинаковый сразу у всех посетителей сайта.

Проверяем библиотеки вывода графики:

printf("картинки=%s шрифты=%s\n",
function_exists('imagecreatetruecolor') ? 'есть' : 'нет',
function_exists('imagettftext') ? 'есть' : 'нет');
// без обеих функций капча не рисуется и форма становится непроходимой

Отсутствие любой из двух функций делает капчу невидимой. Посетитель видит сломанную картинку и пустое поле, а в журнале сервера остаётся ошибка вызова несуществующей функции.

Ограничения

Картинка с кодом останавливает массового бота, но не живого человека. Ручной спам через обычную форму она не отсекает, и с ним работают уже модерацией заявок и стоп-листом адресов.

Внешние сервисы проверки посетителя тянут на страницу чужой скрипт с чужого сервера. Сайт начинает зависеть от доступности этого сервиса, а форма перестаёт работать, как только скрипт не загрузился.

Блокировка по сетевому адресу бьёт заодно и по живым посетителям сайта. За общим адресом сидят целые офисы и мобильные операторы, поэтому срок блокировки держат коротким, а порог - разумным.

Журнал отклонённых заявок заводят с первого дня работы защиты. Без него невозможно понять, что защита отсекает реальные заявки, и жалоба «мы отправляли, вы не ответили» останется неразрешимой.

Типичные проблемы

Спам продолжается, хотя капча включена.

Введённый код с картинки нигде не проверяется на стороне сервера. Бот отправляет запрос прямо на адрес обработчика и разметку формы вообще не читает.

Вместо картинки с кодом пустое место.

На сервере нет расширений вывода графики и работы со шрифтами. Капча рисуется библиотекой изображений, и без неё форму пройти невозможно.

После неудачного входа войти больше не получается.

Система требует код с картинки, а шаблон формы авторизации его не рисует. Поле для ввода кода в таком шаблоне приходится добавлять руками.

Повторная отправка формы всегда сообщает о неверном коде.

При ошибке заполнения форма возвращается посетителю со старым идентификатором картинки. Идентификатор одноразовый, и при каждой отрисовке формы его получают заново.

Заявки от реальных клиентов перестали приходить.

Ограничение частоты отправок или блокировка по адресу настроены заметно жёстче нужного. Без журнала отклонений такая потеря обнаруживается только по жалобам самих клиентов.

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

Где включается капча на штатных формах?

В параметрах компонента формы есть защита от автоматических сообщений, а регистрация и восстановление пароля включают её в настройках главного модуля. Вход просит код после нескольких неудачных попыток, и это настройка группы пользователей.

Достаточно ли одного скрытого поля?

Против примитивных ботов - да, и стоит оно ничего. Целевой спам, написанный под конкретный сайт, ловушку обходит, поэтому её держат как первый слой, а не как единственный.

Можно ли обойтись без картинки с кодом?

Можно, если хватает ловушки, ограничения частоты и модерации заявок. Картинка мешает и живому посетителю, поэтому её ставят там, где поток спама уже мешает работать.

Почему капча не спасает от заявок вручную?

Человек проходит любую проверку на человечность, потому что он и есть человек. Против ручного спама работают модерация, стоп-лист адресов и ограничение частоты.

Как понять, сколько заявок отсекается?

Записью каждого отклонения в отдельный журнал с адресом и причиной. Числа из журнала показывают и объём спама, и случайные потери настоящих обращений.

Смежное

Первоисточник