CSRF-атака змушує авторизованого користувача виконати дію, якої він не планував: змінити налаштування, створити запис, оновити профіль або видалити дані. Браузер при цьому може автоматично додати до запиту cookies WordPress, тому сервер сприймає його як запит від легітимного користувача.
У WordPress основний захист від такого сценарію — nonce, тобто спеціальний токен, пов’язаний із конкретною дією, користувачем і сесією. Він потрібен у власних плагінах, віджетах Elementor, адміністративних формах, AJAX-обробниках і кастомних REST-запитах.
Що таке CSRF і чому він небезпечний для WordPress
CSRF, або Cross-Site Request Forgery, виникає тоді, коли сторонній сайт або шкідливий скрипт змушує браузер користувача надіслати запит до вашого WordPress. Якщо користувач авторизований, cookies можуть бути прикріплені автоматично.
Наприклад, зловмисник може підготувати форму, яка надсилає POST-запит на обробник зміни налаштувань. Користувач відкриває сторінку, натискає кнопку або навіть потрапляє на неї через автоматичну відправку форми — а WordPress бачить дійсну сесію.
Ризик особливо високий для запитів, які змінюють стан системи: створюють або видаляють записи, змінюють email і пароль, оновлюють налаштування, змінюють замовлення чи дані профілю. Просте читання публічних даних зазвичай не є типовою CSRF-ціллю, але всі операції зі зміною даних потрібно захищати.
- CSRF використовує вже наявну авторизацію користувача.
- Перевірки лише наявності cookies недостатньо.
- Захищати потрібно кожну окрему дію, а не тільки сторінку загалом.
Як nonce захищає форму WordPress
Nonce у WordPress — це токен, який підтверджує, що запит, найімовірніше, сформовано вашим інтерфейсом для конкретної дії. Токен пов’язаний з action-рядком, користувачем і часовим вікном, тому випадковий сторонній сайт не повинен знати його значення.
Назва nonce походить від англійського «number used once», але в WordPress це не одноразовий токен у строгому криптографічному сенсі. Він дійсний протягом обмеженого періоду та може бути використаний повторно в межах цього періоду. Тому nonce не слід сприймати як заміну одноразовому коду, паролю або механізму авторизації.
Для кожної операції використовуйте окремий зрозумілий action, наприклад save_profile, delete_invoice або update_widget_settings. Не варто застосовувати один загальний nonce для всіх адміністративних дій.
- Nonce захищає від підробки запиту, але не визначає, чи має користувач право виконувати дію.
- Action має описувати конкретну операцію.
- Відсутній, прострочений або неправильний nonce має призводити до відхилення запиту.
<?php
wp_nonce_field( 'save_profile', 'profile_nonce' );
?>
Додавання nonce у звичайну форму
Функція wp_nonce_field() створює приховане поле з nonce і, за потреби, додаткове поле для перевірки реферера. Розміщуйте її всередині форми, яка виконує зміну даних.
<?php if ( current_user_can( 'edit_user', get_current_user_id() ) ) : ?>
<form method="post">
<?php wp_nonce_field( 'save_profile', 'profile_nonce' ); ?>
<input type="email" name="email" required>
<button type="submit">Зберегти</button>
</form>
<?php endif; ?>
Перевірка nonce і прав на сервері
Приховане поле саме по собі нічого не захищає. Серверний обробник має отримати токен, перевірити його до виконання небезпечної операції, а потім окремо перевірити права поточного користувача.
Для адміністративних форм часто використовують check_admin_referer(). Вона перевіряє nonce і може завершити виконання із помилкою, якщо токен недійсний. Для фронтенд-обробників можна використовувати wp_verify_nonce() з явною обробкою результату. Це дає змогу повернути користувачу контрольоване повідомлення замість несподіваного завершення.
Перевірка прав через current_user_can() обов’язкова. Навіть дійсний nonce не означає, що користувач може редагувати конкретний профіль, запис або налаштування.
- Спочатку перевіряйте метод запиту та nonce.
- Після цього перевіряйте capability користувача.
- Не передавайте ID об’єкта без перевірки, чи має користувач право працювати саме з ним.
- Після перевірок санітизуйте та валідуйте вхідні дані.
<?php
if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
wp_die( 'Неприпустимий метод запиту.' );
}
check_admin_referer( 'save_profile', 'profile_nonce' );
if ( ! current_user_can( 'edit_user', get_current_user_id() ) ) {
wp_die( 'Недостатньо прав.' );
}
$email = isset( $_POST['email'] )
? sanitize_email( wp_unslash( $_POST['email'] ) )
: '';
if ( ! is_email( $email ) ) {
wp_die( 'Некоректна email-адреса.' );
}
// Тут виконується дозволена операція після всіх перевірок.
?>
Nonce в AJAX-запитах WordPress
AJAX-обробники часто стають джерелом проблем, оскільки логіка винесена в окрему функцію, а перевірку безпеки розробник додає пізніше. Для таких запитів передавайте nonce разом із даними та перевіряйте його через check_ajax_referer().
Nonce можна передати в параметрі _ajax_nonce або в іншому узгодженому параметрі. На клієнті його не слід генерувати вручну: сервер має створити значення через wp_create_nonce(), а JavaScript — отримати його з локалізованих даних або іншого безпечного механізму, який формує WordPress.
- Для різних AJAX-дій використовуйте різні action.
- Не вважайте сам факт AJAX-запиту ознакою його легітимності.
- Після перевірки nonce все одно перевіряйте права і параметри операції.
- Для неавторизованих користувачів окремо визначте, чи справді їм потрібен доступ до обробника.
<?php
wp_localize_script(
'profile-script',
'ProfileData',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'save_profile_ajax' ),
)
);
function save_profile_ajax() {
check_ajax_referer( 'save_profile_ajax', 'nonce' );
if ( ! is_user_logged_in() || ! current_user_can( 'edit_user', get_current_user_id() ) ) {
wp_send_json_error( array( 'message' => 'Недостатньо прав.' ), 403 );
}
$email = isset( $_POST['email'] )
? sanitize_email( wp_unslash( $_POST['email'] ) )
: '';
if ( ! is_email( $email ) ) {
wp_send_json_error( array( 'message' => 'Некоректні дані.' ), 400 );
}
// Операція зі зміни даних.
wp_send_json_success();
}
add_action( 'wp_ajax_save_profile', 'save_profile_ajax' );
?>
Особливості Elementor і власних віджетів
Стандартна форма Elementor або сторонній віджет може мати власні механізми обробки, але це не означає, що custom action автоматично захищений. Якщо ваш код приймає дані через hook, AJAX або власний endpoint і змінює стан WordPress, nonce потрібно додати та перевірити саме у вашій логіці.
У власному віджеті не покладайтеся на приховане поле, назву кнопки чи структуру HTML. Зловмисник може сформувати запит без браузерної форми. Безпека має бути реалізована на сервері, незалежно від того, чи прийшов запит із Elementor, звичайного шаблону або зовнішнього клієнта.
Якщо віджет лише надсилає повідомлення на email без зміни авторизованих даних, модель загроз може бути іншою. Проте все одно потрібні перевірка вхідних значень, обмеження частоти, захист від спаму та коректна робота з персональними даними.
- Перевіряйте nonce у тому PHP-обробнику, який реально виконує дію.
- Не вважайте Elementor або інший конструктор заміною серверної авторизації.
- Перевіряйте, чи належить переданий запис, замовлення або профіль поточному користувачу.
- Не виводьте у відповідях зайві внутрішні дані або текст PHP-помилок.
REST API, Referer і додаткові рівні захисту
Для кастомних REST-маршрутів WordPress потрібно визначити permission_callback і перевіряти права в ньому. Якщо використовується cookie-авторизація з адміністративного інтерфейсу, застосовується REST nonce, який зазвичай передають у заголовку X-WP-Nonce. Сам заголовок не замінює перевірку дозволів.
HTTP-заголовок Referer може бути корисним додатковим сигналом, але не повинен бути єдиним захистом. Він може бути відсутнім через налаштування приватності, політики браузера або мережеві особливості. Так само не можна покладатися лише на приховані поля форми: їх легко підробити.
Для зменшення ризику додатково використовуйте HTTPS, коректні cookie-параметри Secure і HttpOnly, розумну політику SameSite, перевірку Origin там, де це доречно, та обмеження частоти для чутливих операцій. Ці заходи доповнюють nonce, але не скасовують його.
- REST-маршрут без permission_callback не повинен виконувати захищені операції.
- Не використовуйте nonce як API-ключ або довгостроковий секрет.
- Не записуйте nonce у логи, публічні відповіді чи сторонні аналітичні системи без потреби.
<?php
register_rest_route(
'site/v1',
'/profile',
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => 'site_update_profile',
'permission_callback' => function () {
return is_user_logged_in()
&& current_user_can( 'edit_user', get_current_user_id() );
},
)
);
?>
Типові помилки під час захисту форм
Найчастіша помилка — додати nonce у форму, але не перевірити його в обробнику. У такому випадку токен є декоративним елементом і не впливає на безпеку.
Інша проблема — перевіряти nonce, але пропускати авторизацію та контроль доступу. Користувач із дійсним токеном не повинен отримувати можливість змінювати чужі дані лише тому, що запит сформовано з вашого інтерфейсу.
Також небезпечно використовувати один nonce для всіх операцій, довіряти параметрам із клієнта, не перевіряти метод запиту та змішувати захист від CSRF із захистом від XSS. Nonce не санітизує HTML, не очищає SQL-запити й не захищає від викрадення сесії.
- Nonce є, але action у генерації та перевірці різний.
- Токен перевіряється після виконання небезпечної операції.
- Перевіряється роль або авторизація, але не capability для конкретної дії.
- Дані з $_POST або $_GET використовуються без wp_unslash(), санітизації та валідації.
- Невдалу перевірку ігнорують, щоб форма «працювала» за будь-яких умов.
- Захист присутній у HTML, але відсутній в AJAX або REST-обробнику.
Поширені запитання
Чи достатньо nonce для захисту форми WordPress?
Ні. Nonce захищає від підробки запиту, але не замінює авторизацію та перевірку прав. Потрібно також перевіряти current_user_can(), валідність даних, належність об’єкта користувачу та безпечну обробку результату.
Чи потрібно додавати nonce до кожної форми?
Nonce потрібен для форм і запитів, які змінюють стан системи або виконують чутливу дію. Для кожної логічної операції бажано використовувати власний action, а не один спільний токен для всього сайту.
Чим відрізняються check_admin_referer() і check_ajax_referer()?
Обидві функції перевіряють nonce, але призначені для різних сценаріїв. check_admin_referer() зазвичай використовують для адміністративних форм, а check_ajax_referer() — для AJAX-запитів. Вибір функції не скасовує окрему перевірку прав.
Чи захищає nonce від XSS або SQL-ін’єкцій?
Ні. Nonce призначений саме для протидії CSRF. Від XSS потрібні екранування під час виведення та безпечна робота з HTML, а від SQL-ін’єкцій — підготовлені запити й коректна валідація даних.
Чому nonce іноді стає недійсним після довгого відкриття сторінки?
Nonce має обмежене часові вікно дії та може залежати від поточної сесії. Якщо сторінка довго відкрита або сесія змінилася, токен може бути відхилений. У такому випадку форму потрібно оновити або передбачити коректне повідомлення користувачу.
Висновок
Захист WordPress-форми від CSRF складається не з одного прихованого поля, а з послідовності перевірок: nonce для конкретної дії, авторизація, capability, контроль об’єкта, валідація даних і безпечне виконання операції.
Для власних плагінів, Elementor-віджетів, AJAX і REST-маршрутів перевіряйте захист на сервері та тестуйте запити без штатної форми. Якщо обробник відхиляє запит без nonce, не дозволяє зайву дію та не довіряє даним із браузера, ризик CSRF суттєво зменшується.