☰
WEB SECURITY / PRACTICE

Защита форм WordPress от CSRF-атак с помощью nonce

Защита форм WordPress от CSRF-атак с помощью nonce

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 или сторонний виджет могут иметь собственные механизмы обработки, но это не означает, что пользовательский 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, корректные атрибуты cookies 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 при генерации отличается от 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 существенно снижается.

← Все материалы веб-безопасности