Защита сайта на MODX от спама
Форма обратной связи на компоненте Formit в использование обертки AjaxForm
Для отправки сообщений с сайта на MODx разработчиками на этой CMF активно используются два компонента Formit и AjaxForm, второй компонент на момент написания статьи (2024 г) уже устарел и не поддерживается несколько лет, взамен ему лучше использовать FetchIt, т.к. AjaxForm имеет явную уязвимость и пропускает спам, который может парализовать отдел продаж клиента бессмысленными «заявками». Но если у вас есть клиентские сайта с компонентом Ajax и переделывать их на новый компонент нет времени и клиент не согласен оплачивать доработки, тогда эта статья для вас.
Защита скрытым полем
Для защиты форм обратной связи от спама без использования капчи и прочих раздражающих ваших посетителей «удобств» адекватные разработчики используют скрытые input в верстке форм, бот попадая на страницу видит форму и все поля, пытается по максимуму все заполнить, тут та мы его и ловим, указывая условие при вызове сниппета Formit о том что это поле должно быть пустым
Вызов сниппета (на его месте появится форм обратной связи)
{$_modx->runSnippet("!AjaxForm", [
'snippet' => 'FormIt',
'form' => '@FILE chunks/all_theme_parts/callback/no_modal_form.tpl',
'hooks' => 'spam,email,FormItSaveForm',
'emailTpl' => '@FILE chunks/all_theme_parts/callback/email_form.tpl',
'emailSubject' => 'Заказ звонка с сайта '~$http_host,
'emailTo' => $contact_email_for_orders,
'emailFrom' => 'lead@'~$http_host,
'formName' => 'Отправка формы обратной связи с сайта '~$http_host,
'validate' => 'page:required,phone:required,username:blank',
])}
Верстка формы которую использует сниппет
Имя файла no_modal_form.tpl
<form action="{$site_url}{$_modx->resource.id | url}" method="post" id="callbackrow" class="ajax_form">
<input type="hidden" name="page" value="{$_modx->resource.pagetitle | htmlent}">
<input type="hidden" name="pageid" value="{$_modx->resource.id}">
<input type="hidden" name="pageurl" value="{$site_url~$_modx->resource.id | url}">
<input type="hidden" id="clientID" name="clientID" value="">
<input type="hidden" name="form_name" value="Запрос обратного звонка">
<div class="modal-body">
<div class="message"></div>
<div class="fields">
<div class="form-group hidden">
<label >Ваше имя
<input type="text" id="callbackform_user" class="form-control" name="username"
placeholder="Пользователь">
</label>
</div>
<div class="form-group pb-2">
<input type="text" class="form-control form-control-lg" id="callbackform_name" name="name"
placeholder="Ваше имя" required>
</div>
<div class="form-group">
<input type="text" class="form-control phone form-control-lg" id="callbackform_phone" name="phone"
placeholder="Ваш телефон*" required>
</div>
<div class="form-group">
<input type="text" class="form-control phone form-control-lg" id="callbackform_email" name="email"
placeholder="Ваш e-mail*" required>
</div>
<div class="form-group">
<textarea class="form-control" name="text" id="callbackform_text" placeholder="Ваше сообщение или вопрос" required></textarea>
</div>
{/if}
</div>
</div>
<div class="fields">
<div class="row px-3 mb-3 mt-3">
<div class="col-12 text-end">
<button type="submit" class="btn btn-warning btn-xl mt-3">{$btn_text?:'Отправить'}</button>
</div>
</div>
</div>
</form>
Как выглядит форма в браузере
Теперь что бы понять суть защиты формы обратной связи от спама, если еще не ясно, посмотрим на скрины из отладчика браузера, как MODX вывел разметку формы и куда встали поля
Как срабатывает защита от ботов со скрытым полем?
Боты НЕ видят как выглядит сайт в браузере, в том числе и как выглядит форма, они видят html код, видят что есть теги FORM там ищут INPUT и составляют запрос на основе этих полей, что бы не получить ошибку обязательного поля, т.к. часто некоторые поля являются обязательными бот будет заполнять все поля, и исходя из названия поля (phone, user, username, lastname) подставляют свои значения которые определил его хозяин-спамер. Тут та и срабатывает наша уловка, бот видит INPUT в значении которого name=“username“, полагая что это обязательное поле для заполнение он запихнет туда свое значение из настроек, но то что произойдет дальше, наш тупой бот не ожидает, сниппет FORMIT уже предупрежден, что если поле с name=“username“ не пустое то тогда ни чего не нужно отправлять и выдать ошибку, которую конечно же бот не увидит.
Вот эта часть в вызове Formit:
- 'validate' => 'page:required,phone:required,username:blank',
Если еще не ясно почему это поле не заполняют реальные пользователи, то обратите внимание на верхнем скрине на класс поля hidden и его значение display:none, эта конструкция говорит браузеру скрыть поле, в коде его видно а в браузере после рендера не видно, поэтому реальный человек его увидеть не сможет, а соответственно и заполнить.
Спам все равно приходит даже со скрытыми полями в MODX и компоненте Formit
Такая проблема известна с марта 2024 года, возможно такие случаи были зафиксированы и ранее, в целом проблема в выполнении скрипта из папки AjaxForm при прямом обращении к нему по адресу
/assets/components/ajaxform/action.php
Это можно проверить в логах веб сервера, если у вас стоит какой либо плагин логирования для MODX типа antibot или самописное решение, то эти обращения он не зафиксирует, т.к. нет обращения к index.php а вместо него прямое обращение к файлу action.php
Решение проблемы спама при прямом обращении к action.php
Нужно создать сниппет который запускается в прехуках Formit и создает в cookie пользователя переменную, браузер пользователя получит эту переменную только при запуске вашего сниппета, в котором вы укажите preHooks в параметрах вызова. Назовем сниппет к примеру checkBotDirect, на скрине ниже
Код сниппета.
<?php
$_SESSION['trueuser'] = 1;
return true;
Теперь нужно добавить прехук в вызов нашего сниппета
Это строчка:
'preHooks' => 'checkBotDirect',
При нажатии кнопки отправить в форме обратной связи (которую формирует наш сниппет) сначала запускаются сниппеты указанные через запятую в этом параметре, в нашем случае запустится сниппет и исполнит php код который создаст в куках пользователя переменную trueuser = 1;
{$_modx->runSnippet("!AjaxForm", [
'snippet' => 'FormIt',
'preHooks' => 'checkBotDirect',
'form' => '@FILE chunks/all_theme_parts/callback/no_modal_form.tpl',
'hooks' => 'spam,email,FormItSaveForm',
'emailTpl' => '@FILE chunks/all_theme_parts/callback/email_form.tpl',
'emailSubject' => 'Заказ звонка с сайта '~$http_host,
'emailTo' => $contact_email_for_orders,
'emailFrom' => 'lead@'~$http_host,
'formName' => 'Отправка формы обратной связи с сайта '~$http_host,
'validate' => 'page:required,phone:required,username:blank',
])}
Изменяем файл action.php а папке ajaxForm
Идем в корень сайта и переходим по пути:
/assets/components/ajaxform/action.php
Файл action.php отвечает за отправку всех форм обратной связи, давайте сделаем так что бы он не отправлял ничего если переменной trueuser нет в куках. Для этого с 23 строчки этого файла добавим логику проверки переменной, и в случае если она есть продолжаем штатное выполнение скрипта, а если ее нет, то значит перед нами бот, который напрямую обратился к скрипту и он не был на нашем сайте, а возможно это просто бот без cookie что в целом нас тоже устраивает.
Можно скопировать скрипт полностью и заменить содержимое в своем файле
<?php
/** @var modX $modx */
define('MODX_API_MODE', true);
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/index.php';
$modx->getService('error', 'error.modError');
$modx->setLogLevel(modX::LOG_LEVEL_ERROR);
$modx->setLogTarget('FILE');
// Switch context if need
if (!empty($_REQUEST['pageId'])) {
if ($resource = $modx->getObject('modResource', (int)$_REQUEST['pageId'])) {
if ($resource->get('context_key') != 'web') {
$modx->switchContext($resource->get('context_key'));
}
$modx->resource = $resource;
}
}
/** @var AjaxForm $AjaxForm */
$AjaxForm = $modx->getService('ajaxform', 'AjaxForm', $modx->getOption('ajaxform_core_path', null,
$modx->getOption('core_path') . 'components/ajaxform/') . 'model/ajaxform/', array());
//проверяем на бота
if($_SESSION['trueuser'] != 1){ //не прошел проверку
echo $AjaxForm->success('Сообщение успешно отправлено.'); //скажем что все ок =)))
//Запишем в логи журнала MODX всех кто попытлся обратиться напрямую к скрипту
//можно закомментировать или удалить это условие если не нужны логи
// Определение IP-адреса пользователя, учитывая Cloudflare
if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ipAddress = $_SERVER['HTTP_CF_CONNECTING_IP'];
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
$ipAddress = $_SERVER['REMOTE_ADDR'];
} else {
$ipAddress = 'Unknown IP';
}
// Логирование ошибки проверки поля username
$modx->log(xPDO::LOG_LEVEL_ERROR, 'Обращение к action.php напрямую неудачное');
$modx->log(xPDO::LOG_LEVEL_ERROR, 'IP-адрес: ' . $ipAddress);
$modx->log(xPDO::LOG_LEVEL_ERROR, print_r($_POST, true));
die(); //выходим из скрипта т.к. нет переменной в куках
}
unset($_SESSION['trueuser']);
if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] != 'XMLHttpRequest') {
$modx->sendRedirect($modx->makeUrl($modx->getOption('site_start'), '', '', 'full'));
} elseif (empty($_REQUEST['af_action'])) {
echo $AjaxForm->error('af_err_action_ns');
} else {
echo $AjaxForm->process($_REQUEST['af_action'], array_merge($_FILES, $_REQUEST));
}
@session_write_close();
Итог
Мы сделал кастомное решение для защиты от спама своего сайта или сайта клиента, за последнее время мы проделывали такую защиту несколько раз, скрытые поля не помогают, все чаще спам идет через обращение к action.php, какую цель преследую те кто это делает не совсем понятно, чаще всего на боевых клиентских проектах в первую очередь страдает отдел продаж, фейковые заявки с реальными номерами телефонов могут саботировать работу продажников, а если при этом у вас еще работает Яндекс Директ то реальные заявки вы упустите, т.к. они потеряются в куче спамных. Это явление часто происходит в высококонкурентных нишах, типа пластиковых окон или строительстве, скорее всего стараются нейтрализовать как можно больше конкурентов, других логических целей я не вижу, рассылать спам на другие e-mail не получится, не на столько уязвим компонет.
В целом если вам все понятно, но вы не можете реализовать защиту MODX от спама, можете обратиться к нам, мы сделаем вам под ключ решение с защитой вашего сайта от спама