Skip to content
  1. Компоненты
  2. FetchIt
  3. Защита от спама

Защита от спама ​

Каждая форма FetchIt защищена сразу после установки, настраивать ничего не нужно. Проверки проходят до FormIt или вашего сниппета — и при отправке через FetchIt, и при обычной отправке формы без JavaScript.

Выключает всё настройка fetchit.protection. Выключать её стоит только для отладки.

Что проверяется всегда ​

Токен. В форму добавляется скрытое поле fetchit_token с подписью ключа формы, времени вывода страницы и случайного числа. Токен одноразовый, сессия для него не нужна. Новый токен выдаётся только в ответ на правильно подписанный токен этой формы, так что бот, который не загружал страницу, форму не отправит. Ответ на отправку приносит следующий токен в заголовке X-FetchIt-Token, и форму можно отправлять повторно без перезагрузки.

Время заполнения. Форма, отправленная быстрее fetchit.protection.min_time секунд после вывода страницы (по умолчанию 3), отклоняется. Повторная отправка считается от того же вывода, поэтому человек, который исправил поле после ошибки, отказ не получит.

Ловушка. Скрытое поле со случайным для каждой установки именем: люди его не видят, автозаполнение браузера не узнаёт. Если оно заполнено, бот получает ответ об успешной отправке с successMessage формы, но форма не обрабатывается и письмо не уходит. Такие случаи пишутся в журнал MODX.

Лимит. Не больше fetchit.protection.rate_limit отправок одной формы с одного адреса за fetchit.protection.rate_window секунд — по умолчанию 10 за 10 минут. Считаются все попытки с правильно подписанным токеном, в том числе отклонённые, так что повтором старого токена лимит не обойти.

Служебные поля убираются из $_POST до FormIt, в письма они не попадают. Ключ подписи генерируется при установке пакета и лежит в fetchit.protection.secret.

Proof-of-work ​

Выключен по умолчанию, нужен, если спам проходит через основные проверки. Работает, только когда включена fetchit.protection.

fetchit.protection.pow задаёт сложность в битах: 0 выключает, больше 24 не бывает — значение сверх этого или не число пишется в журнал и заменяется. Перед отправкой браузер ищет число n, для которого SHA-256 от токен:n начинается с этого количества нулевых бит, и отправляет его в поле fetchit_pow.

Решение начинается, как только посетитель зашёл в форму, так что к нажатию кнопки оно обычно готово, а боту каждая отправка стоит работы процессора. При 16 битах в среднем нужно около 65 тысяч хешей: компьютеру примерно треть секунды, телефону в несколько раз больше. Каждый следующий бит удваивает время, 20 бит — примерно в 16 раз дольше.

Если страница из кеша просит меньше, чем сервер, FetchIt решит задачу заново и отправит форму ещё раз.

Внимание

Форму без JavaScript при включённом proof-of-work отправить нельзя.

Капча ​

В fetchit.captcha укажите сервис, а в fetchit.captcha.site_key и fetchit.captcha.secret_key — ключи из его кабинета:

ЗначениеСервисКак работает
turnstileCloudflare TurnstileВиджет в блоке .fetchit-captcha перед первой кнопкой type="submit", а если такой нет — в конце формы
recaptchaGoogle reCAPTCHA v3Без виджета, ответ запрашивается при каждой отправке с действием fetchit. Оценка ниже fetchit.captcha.min_score (по умолчанию 0.5) отклоняется
smartcaptchaЯндекс SmartCaptchaНевидимый режим, задание показывается, только если сервис сомневается

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

Ответ проверяется на сервере последним из проверок защиты (плагины OnFetchItBeforeProcess вызываются после него), так что боты без токена до сервиса капчи не доходят.

Внимание

Капча работает только через FetchIt: форма без JavaScript её не пройдёт.

Для проверки на сайте разработки у Turnstile есть тестовые ключи: сайт 1x00000000000000000000AA, секрет 1x0000000000000000000000000000000AA.

Что видит посетитель ​

Отказ приходит как обычная ошибка формы: сообщение из лексикона, событие fetchit:error и уведомление.

Ключ лексиконаКогда
fetchit_err_tokenформа устарела: нет токена или он не подходит
fetchit_err_too_fastотправлено быстрее min_time
fetchit_err_rateпревышен лимит отправок
fetchit_err_storeне удалось записать отметку об использованном токене
fetchit_err_powне сошёлся proof-of-work
fetchit_err_captchaсервис капчи не принял ответ
fetchit_err_captcha_unavailableсервис капчи недоступен или не принял секретный ключ
fetchit_err_captcha_clientкапча в браузере не дала ответ: блокировщик, CSP, посетитель закрыл задание

Если токен страницы устарел — страница из кеша, была долго открыта, сменился ключ, — FetchIt один раз отправит форму заново с новым токеном, и посетитель ничего не заметит.

Без JavaScript сообщение выводится в [[+fi.validation_error_message]], а введённые значения сохраняются, как в чанке tpl.FetchIt.example из пакета.

Что стоит учесть ​

  • Кеш всей страницы — nginx, CDN, плагины статического кеша — отдаёт всем посетителям один и тот же токен. Через FetchIt это работает за счёт повторной отправки, но страницы с формами лучше исключить из такого кеша: без JavaScript первая отправка будет отклонена.
  • FormIt 5.2 и новее умеет отправлять формы через AJAX сам. Для форм FetchIt этот режим выключается: FetchIt убирает сохранённые параметры FormIt, плейсхолдер fi.ajaxToken и скрипт formit.js, иначе форму можно было бы отправить через action.php FormIt в обход защиты. Формы, которые на той же странице выводит сам [[!FormIt]], свой AJAX-режим сохраняют. reCAPTCHA из FormIt 5.2 получает ответ через formit.js, поэтому в формах FetchIt она не работает — включайте капчу через fetchit.captcha.
  • Формы, вставленные на страницу через AJAX после её загрузки, FetchIt сам не подхватывает: скрипт находит формы при загрузке страницы. Такие формы отправятся обычным способом.
  • За прокси или CDN все посетители приходят с одного адреса, и лимит становится общим на сайт. Перечислите адреса прокси в fetchit.protection.proxies (IP или CIDR через запятую) и заголовок с адресом посетителя в fetchit.protection.ip_header (X-Forwarded-For, CF-Connecting-IP).
  • Отметки использованных токенов хранятся в core/cache/fetchit/tokens/, «Очистить кеш» их не трогает. Если папка недоступна для записи, формы отклоняются, а причина пишется в журнал. На нескольких серверах без общего core/cache токен можно использовать по разу на каждом сервере.
  • Журнал fetchit.protection.log: 0 выключает, 1 (по умолчанию) пишет проблемы и отказы, которые могут задеть людей (ловушка, лимит, запись отметок, недоступный сервис капчи), 2 пишет все отказы, в том числе ответы капчи, которые сервис не принял. Ошибки в настройках капчи и proof-of-work пишутся всегда.
  • На сайте для разработки и в автотестах поставьте fetchit.protection.min_time и fetchit.protection.rate_limit в 0. Выключать всю защиту стоит только для отладки.

Свой JavaScript вместо встроенного скрипта ​

Свой скрипт должен отправлять форму на action.php с заголовком X-FetchIt-Action, равным атрибуту data-fetchit формы, и полем pageId с id страницы. Кроме того:

  • отправлять поле fetchit_token из формы и после каждого ответа брать следующий токен из заголовка X-FetchIt-Token;
  • если ответ пришёл с заголовком X-FetchIt-Refused: token, отправить форму ещё раз с новым токеном;
  • с proof-of-work — отправлять в fetchit_pow решение для того токена, который уходит в этом запросе, и решать заново при повторе. При отказе pow заголовок X-FetchIt-Pow сообщает нужную сложность;
  • с капчей — отправлять ответ сервиса в его поле (cf-turnstile-response, g-recaptcha-response или smart-token), а reCAPTCHA v3 выполнять с действием fetchit.

Свои правила ​

Плагин на событие OnFetchItBeforeProcess получает:

ПараметрЧто в нём
$actionключ формы
$fieldsотправленные поля без служебных и без файлов
$propertiesпараметры вызова сниппета, может быть null
$FetchItэкземпляр сервиса

Чтобы отклонить отправку, плагин выводит сообщение для посетителя или ключ лексикона через $modx->event->output(). Возврат строки через return отправку не отклоняет: MODX только запишет её в журнал.

php
// Плагин на событие OnFetchItBeforeProcess
$email = isset($fields['email']) && is_string($fields['email']) ? $fields['email'] : '';
if (preg_match('/@(mailinator|tempmail)\./i', $email)) {
    $modx->event->output('Одноразовые адреса не принимаются.');
}

Событие срабатывает и при выключенной защите.