
Защита от спама
Каждая форма 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 — ключи из его кабинета:
| Значение | Сервис | Как работает |
|---|---|---|
turnstile | Cloudflare Turnstile | Виджет в блоке .fetchit-captcha перед первой кнопкой type="submit", а если такой нет — в конце формы |
recaptcha | Google 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.phpFormIt в обход защиты. Формы, которые на той же странице выводит сам[[!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 только запишет её в журнал.
// Плагин на событие OnFetchItBeforeProcess
$email = isset($fields['email']) && is_string($fields['email']) ? $fields['email'] : '';
if (preg_match('/@(mailinator|tempmail)\./i', $email)) {
$modx->event->output('Одноразовые адреса не принимаются.');
}Событие срабатывает и при выключенной защите.
