Проверка nonce в REST API

22 дек. 2018 г., 03:25:13
Просмотры: 21.6K
Голосов: 9

Я хотел бы разобраться в лучших практиках проверки nonce в REST API.

Я вижу, что многие говорят о nonce wp_rest для REST-запросов. Но при изучении исходного кода WordPress я обнаружил, что wp_rest - это просто nonce для проверки статуса авторизованного пользователя, а если его нет, запрос выполняется от имени гостя.

Учитывая это, должен ли я отправлять два nonce при выполнении POST-запроса к REST API? Один для аутентификации wp_rest и другой для действия foo_action?

Если да, то как мне отправить nonce wp_rest и foo_action в JavaScript, и в каком месте PHP-кода правильно проверять эти nonce? (Имею в виду validate_callback для аргумента или permission_callback?)

0
Все ответы на вопрос 3
4
12

Вам следует передать специальный одноразовый код wp_rest (nonce) как часть запроса. Без него объект global $current_user не будет доступен в вашем REST-классе. Вы можете передать его несколькими способами: через $_GET, $_POST или заголовки.

Действующий одноразовый код (action nonce) является опциональным. Если вы его добавите, то не сможете использовать REST-эндпоинт с внешнего сервера — только из запросов, отправленных из самого WordPress. Пользователь может аутентифицироваться с помощью Basic Auth, OAuth2 или JWT с внешнего сервера даже без одноразового кода wp_rest, но если вы добавите и action nonce, это не сработает.

Таким образом, action nonce является опциональным. Добавляйте его, только если хотите, чтобы эндпоинт работал локально.

Пример:

/**
* Первый шаг: регистрация, локализация и подключение JavaScript
*/
wp_register_script( 'main-js', get_template_directory_uri() . '/js/main.js', [ 'jquery' ] );
wp_localize_script( 'main-js', 'data', [
    'rest' => [
        'endpoints' => [
            'my_endpoint'       => esc_url_raw( rest_url( 'my_plugin/v1/my_endpoint' ) ),
        ],
        'timeout'   => (int) apply_filters( "my_plugin_rest_timeout", 60 ),
        'nonce'     => wp_create_nonce( 'wp_rest' ),
        //'action_nonce'     => wp_create_nonce( 'action_nonce' ), 
    ],
] );
wp_enqueue_script( 'main-js' );

/**
* Второй шаг: запрос в JavaScript-файле
*/
jQuery(document).on('click', '#some_element', function () {
    let ajax_data = {
        'some_value': jQuery( ".some_value" ).val(),
        //'action_nonce': data.rest.action_nonce
    };

    jQuery.ajax({
        url: data.rest.endpoints.my_endpoint,
        method: "GET",
        dataType: "json",
        timeout: data.rest.timeout,
        data: ajax_data,
        beforeSend: function (xhr) {
            xhr.setRequestHeader('X-WP-Nonce', data.rest.nonce);
        }
    }).done(function (results) {
        console.log(results);
        alert("Успешно!");
    }).fail(function (xhr) {
        console.log(results);
        alert("Ошибка!");
    });
});

/**
* Третий шаг: сам REST-эндпоинт
*/
class My_Endpoint {
        public function registerRoutes() {
        register_rest_route( 'my_plugin', 'v1/my_endpoint', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [ $this, 'get_something' ],
            'args'                => [
                'some_value'     => [
                    'required' => true,
                ],
            ],
            'permission_callback' => function ( WP_REST_Request $request ) {
                return true;
            },
        ] );
    }

    /**
    *   @return WP_REST_Response
    */
    private function get_something( WP_REST_Request $request ) {
        //if ( ! wp_verify_nonce( $request['nonce'], 'action_nonce' ) ) {
        //  return false;
        //}

        $some_value        = $request['some_value'];

        if ( strlen( $some_value ) < 5 ) {
            return new WP_REST_Response( 'Извините, Some Value должен содержать не менее 5 символов.', 400 );
        }

        // Поскольку мы передаем заголовок "X-WP-Nonce", это сработает:
        $user = wp_get_current_user();

        if ( $user instanceof WP_User ) {
            return new WP_REST_Response( 'Извините, не удалось получить имя.', 400 );
        } else {
            return new WP_REST_Response( 'Ваше имя пользователя: ' . $user->display_name, 200 );
        }
    }
}
22 дек. 2018 г. 03:25:13
Комментарии

Важно также добавить 'required' => true к аргументам foo_nonce, иначе можно будет отправлять запросы к конечной точке без nonce.

Dylan Dylan
5 июн. 2019 г. 06:04:20

Откуда берется data.rest.nonce? Я не вижу его определения в вашем коде.

drwatsoncode drwatsoncode
10 янв. 2020 г. 06:53:09

@Dylan Хорошая идея сделать его обязательным, но если вы не отправите nonce действия, запрос завершится ошибкой при проверке wp_verify_nonce, которая выполняется перед запуском действия

Lucas Bustamante Lucas Bustamante
17 окт. 2021 г. 23:25:29

@drwatsoncode data.rest.nonce берется из wp_localize_script в примере кода

Lucas Bustamante Lucas Bustamante
17 окт. 2021 г. 23:39:03
0

Развивая то, что написал @lucas-bustamante (это очень помогло!), после настройки заголовка X-WP-Nonce в ваших пользовательских маршрутах, вы можете сделать следующее:

    register_rest_route('v1', '/my_post', [
        'methods' => WP_REST_Server::CREATABLE,
        'callback' => [$this, 'create_post'],
        'args' => [
            'post_title' => [
                'required' => true,
            ],
            'post_excerpt' => [
                'required' => true,
            ]
        ],
        'permission_callback' => function ( ) {
            return current_user_can( 'publish_posts' );
        },
    ]);

Обратите внимание, что permission_callback находится на корневом уровне, а не под args (документация здесь), и я убрал дополнительную проверку nonce из args, так как проверка разрешения сама по себе завершится ошибкой, если nonce недействителен или не предоставлен (я тщательно протестировал это и могу подтвердить, что получаю ошибку, когда nonce не передан или он недействителен).

26 февр. 2020 г. 14:50:22
0

Важно отметить, что в ответе @Lucas Bustamante описан процесс проверки на основе пользователя. Это означает, что если у вас есть анонимная конечная точка API, которая не требует пользователя, то просто не передавая заголовок X-WP-NONCE, вы пройдёте описанную проверку nonce. Однако, если передать некорректный nonce, всё равно будет вызвана ошибка.

Причина в том, что функция rest_cookie_check_errors, которая выполняет проверку, просто устанавливает current_user в пустое значение, если nonce не предоставлен. Это работает корректно, когда пользователь требуется, но не в обратном случае. (см.: https://developer.wordpress.org/reference/functions/rest_cookie_check_errors/)

Если вы хотите расширить ответ Lucas, чтобы включить анонимные конечные точки, вы можете добавить ручную проверку nonce в начало вашей конечной точки, например так:

if ( !$_SERVER['HTTP_X_WP_NONCE'] || !wp_verify_nonce( $_SERVER['HTTP_X_WP_NONCE'], 'wp_rest' ) ) {
    header('HTTP/1.0 403 Forbidden');
    exit;
}
15 февр. 2023 г. 14:31:29