В форме заявки на этом сайте нет токена WordPress (nonce). Это не забывчивость: страница с формой лежит в кэше неделю, а токен живёт сутки. Вместо него — три простые проверки, которые с кэшем не спорят.
Это девятая часть цикла «WordPress на кэшируемом сайте» — первая из пяти про формы.
Почему токен в кэшированной форме — бомба
wp_create_nonce выдаёт строку, которая принимается около суток. Если она напечатана в разметке, а разметка живёт в кэше дольше, посетитель получает форму с мёртвым токеном и заявка отклоняется. На одном из рабочих сайтов мы нашли 6 мёртвых токенов из 18 закэшированных форм — разбор и решение с выдачей токена отдельным запросом в 17-й части цикла про кэш фрагментов.
От чего nonce на самом деле защищает
Nonce в WordPress — защита от подделки запроса от имени вошедшего пользователя: чтобы чужой сайт не мог заставить браузер администратора что-то удалить. Токен привязан к пользователю и его сессии.
У анонимного посетителя ни пользователя, ни сессии нет, поэтому токен для всех анонимов в течение 12 часов один и тот же. Бот, которому нужна ваша форма, просто читает его со страницы, как и браузер. От спама анонимный nonce не защищает вовсе — он только добавляет способ потерять настоящую заявку.
Где nonce нужен: действия вошедших пользователей и всё в админке. Где не нужен: анонимная форма заявки, подписки, комментария.
Что вместо него
- поле-ловушка. Невидимое поле «сайт»: человек его не видит, бот заполняет;
- время заполнения. Скрипт считает секунды с открытия страницы и отправляет их вместе с формой. Меньше 3 секунд или поле не пришло (отправка без скрипта) — не человек;
- лимит с адреса. Не больше 5 заявок в час с одного IP.
$elapsed = isset($_POST['elapsed']) ? (int) $_POST['elapsed'] : -1;
if ($field('website', 200) !== '' || $elapsed < 3) {
// Бот: отвечаем «успешно», чтобы не подсказывать, что именно его выдало.
wp_send_json(['ok' => true]);
}
$limitKey = 'contact_form_rl_' . md5($ip);
$sent = (int) get_transient($limitKey);
if ($sent >= 5) {
wp_send_json(['ok' => false, 'message' => 'слишком много заявок подряд, напишите в Telegram']);
}
Настоящий адрес посетителя за прокси — первый в X-Forwarded-For, иначе лимит считался бы по адресу прокси, одному на всех.
Почему этого хватает
Массовый спам по формам идёт скриптами без браузера: они заполняют все поля подряд и отправляют сразу. На такие отправки ловушка и время срабатывают надёжно. Тот, кто вручную пишет бота под конкретный сайт, обойдёт и капчу; лимит с адреса ограничит ущерб.
Главное — ни одна из трёх проверок не зависит от того, когда страница попала в кэш. Форма из кэша недельной давности отправляется так же, как свежая.
Заявка не сбрасывает кэш
Мелочь, которую легко пропустить: заявки у нас — отдельный тип записи, и их сохранение по умолчанию попало бы в общий сброс кэша на save_post. Заявка — не контент сайта, поэтому для неё сброс снят:
// save_post_{тип} срабатывает раньше save_post — снятие успевает
add_action('save_post_lead', function () {
remove_action('save_post', 'cache_purge_on_save_post');
}, 1);
На одном из рабочих сайтов этого не было, и каждая заявка с формы сбрасывала кэш главной.
Заявки теряются, а в журнале про токен?
Пришлите адрес — уберу токен из кэшированной разметки и проверю, что форма отправляется всегда.
// загружаю комментарии…