☰
WORDPRESS / PRACTICE

Як захистити кастомні REST API endpoint’и у WordPress

Як захистити кастомні REST API endpoint’и у WordPress

Кастомний REST API endpoint у WordPress зручно використовувати для AJAX-запитів, мобільного застосунку, особистого кабінету або інтеграції з іншою системою. Але кожен такий маршрут стає окремою точкою доступу до даних. Якщо неправильно налаштувати права або повертати зайву інформацію, endpoint може перетворитися на джерело витоку.

Найнебезпечніший сценарій — залишити permission_callback з __return_true, а в callback повертати замовлення, записи, email користувачів, службові мета-поля чи інші внутрішні дані. Нижче — практичний підхід до реєстрації, перевірки доступу, обробки параметрів і тестування кастомних REST-маршрутів.

Визначте, кому справді потрібен доступ

Перед написанням коду визначте, чи endpoint має бути публічним, доступним автентифікованим користувачам або лише окремій ролі. Публічний маршрут виправданий, коли всі дані в його відповіді призначені для анонімного перегляду: наприклад, список категорій, заголовки опублікованих матеріалів або загальнодоступний статус сервісу.

Якщо endpoint повертає персональні, комерційні або адміністративні дані, доступ потрібно обмежити. При цьому прихований URL не є захистом: маршрут можна знайти в коді JavaScript, документації мобільного застосунку, логах або мережевих запитах браузера.

Важливо розділяти автентифікацію та авторизацію. Автентифікація відповідає на питання, хто користувач. Авторизація — чи має саме цей користувач право виконати конкретну операцію або побачити конкретний об’єкт.

  • Публічні дані: permission_callback може дозволяти доступ без входу.
  • Дані авторизованого користувача: перевіряйте наявність входу та доступ до його об’єктів.
  • Адміністративні або фінансові дані: перевіряйте відповідну capability.
  • Операції з конкретним постом, замовленням або користувачем: додатково перевіряйте право саме на цей об’єкт.

Налаштуйте permission_callback, а не покладайтеся на callback

У register_rest_route() permission_callback має бути окремим бар’єром перед виконанням основної логіки. Не варто спочатку отримувати дані, а потім перевіряти права всередині callback: це ускладнює код і підвищує ризик помилки.

Для приватного endpoint перевіряйте capability, яка відповідає операції. Наприклад, manage_woocommerce доречна для доступу до адміністративних даних WooCommerce, але не повинна автоматично використовуватися для звичайного перегляду власного замовлення клієнтом.

  • Для керування контентом можуть використовуватися edit_posts, publish_posts або edit_post.
  • Для налаштувань сайту зазвичай потрібна capability manage_options, якщо операція справді адміністративна.
  • Для конкретного об’єкта використовуйте перевірки на кшталт current_user_can('edit_post', $post_id).
  • Не замінюйте перевірку прав умовою на кшталт $_GET['token'] === 'секрет': такі токени часто потрапляють у логи, історію браузера або реферери.
register_rest_route('site/v1', '/orders', [
    'methods'  => WP_REST_Server::READABLE,
    'callback' => 'site_get_orders',
    'permission_callback' => function () {
        return current_user_can('manage_woocommerce');
    },
]);

Перевіряйте автентифікацію та права на рівні об’єкта

Перевірки ролі недостатньо для endpoint, який працює з конкретним записом або замовленням. Користувач може мати право переглядати власні дані, але не дані іншого користувача. Тому після отримання ідентифікатора об’єкта потрібно перевірити його належність поточному користувачу або capability для редагування чи перегляду саме цього об’єкта.

Для WordPress із cookie-автентифікацією REST API-запит має містити коректний nonce, зазвичай переданий у заголовку X-WP-Nonce. Nonce допомагає захиститися від CSRF, але не замінює перевірку capability. Для інтеграцій без браузерної сесії застосовують відповідний спосіб автентифікації, наприклад Application Passwords або механізм, передбачений конкретною системою.

Не вважайте CORS або обмеження за User-Agent засобом контролю доступу. Вони можуть впливати на поведінку клієнта, але не замінюють серверну перевірку.

  • Отримайте поточного користувача через WordPress API, а не з параметра запиту.
  • Порівнюйте власника ресурсу з get_current_user_id().
  • Для редагування або видалення перевіряйте capability для конкретного поста чи іншого об’єкта.
  • Повертайте помилку з кодом 401 для неавторизованого запиту та 403, коли користувач увійшов, але не має потрібних прав.
function site_get_order(WP_REST_Request $request) {
    $order_id = absint($request['id']);
    $order = wc_get_order($order_id);

    if (!$order) {
        return new WP_Error(
            'order_not_found',
            'Замовлення не знайдено.',
            ['status' => 404]
        );
    }

    $user_id = get_current_user_id();
    $can_manage = current_user_can('manage_woocommerce');
    $is_owner = (int) $order->get_user_id() === $user_id;

    if (!$can_manage && !$is_owner) {
        return new WP_Error(
            'forbidden',
            'Недостатньо прав для перегляду цього замовлення.',
            ['status' => 403]
        );
    }

    return [
        'id'     => $order->get_id(),
        'status' => $order->get_status(),
        'total'  => $order->get_total(),
    ];
}

Санітизуйте та валідуйте параметри запиту

Параметри REST-запиту не можна вважати безпечними лише тому, що їх передав ваш JavaScript-код. Користувач може відправити запит вручну з будь-якими значеннями. Санітизація очищає дані від небажаного вмісту, а валідація перевіряє, чи відповідають вони очікуваному формату та допустимому діапазону.

Вибирайте обробку відповідно до типу параметра. Для числового ID використовуйте absint() і перевіряйте, що значення не стало нульовим. Для короткого тексту може підійти sanitize_text_field(), а для email — sanitize_email() разом із перевіркою is_email(). Для параметрів зі строго обмеженим набором значень краще застосувати whitelist.

  • absint() — для додатних цілих ідентифікаторів.
  • sanitize_text_field() — для коротких текстових значень без HTML.
  • sanitize_email() та is_email() — для адрес електронної пошти.
  • validate_callback — для перевірки діапазонів, форматів, дат і допустимих значень.
  • Не використовуйте санітизацію як заміну SQL-підготовці, перевірці capability або екрануванню HTML.
register_rest_route('site/v1', '/orders', [
    'methods'  => WP_REST_Server::READABLE,
    'callback' => 'site_get_orders',
    'permission_callback' => function () {
        return current_user_can('manage_woocommerce');
    },
    'args' => [
        'page' => [
            'default'           => 1,
            'sanitize_callback' => 'absint',
            'validate_callback' => function ($value) {
                return (int) $value >= 1;
            },
        ],
        'status' => [
            'sanitize_callback' => 'sanitize_text_field',
            'validate_callback' => function ($value) {
                return in_array($value, ['pending', 'processing', 'completed'], true);
            },
        ],
    ],
]);

Формуйте відповідь за принципом мінімально необхідних даних

Навіть якщо користувач має право викликати endpoint, це не означає, що йому потрібні всі поля об’єкта. Повернення сирого WP_Post, результату get_users(), повного замовлення або масиву мета-полів може розкрити email, телефони, внутрішні ідентифікатори, службові прапорці та інші дані.

Створюйте явний масив відповіді з полями, які потрібні конкретному клієнту. Такий підхід одночасно покращує безпеку, стабільність API та контроль над змінами структури даних.

Особливо обережно працюйте з user_meta, post_meta і даними замовлень. Не повертайте паролі, токени, ключі інтеграцій або значення, що використовуються для внутрішньої логіки сайту.

  • Визначте контракт endpoint: які поля, типи та формати він повертає.
  • Не передавайте об’єкти WordPress без явного перетворення у відповідь.
  • Використовуйте пагінацію для списків і встановлюйте розумний ліміт кількості записів.
  • Повертайте зрозумілі коди та повідомлення помилок без stack trace, SQL-запитів і службових шляхів на production.
  • Розділяйте адміністративний endpoint і публічний endpoint, навіть якщо вони працюють з однаковим типом даних.
function site_format_user(WP_User $user) {
    return [
        'id'          => (int) $user->ID,
        'displayName' => $user->display_name,
        'avatar'      => get_avatar_url($user->ID),
    ];
}

// Не повертайте весь об'єкт $user або результат get_users() без фільтрації.

Захистіть фільтри, сортування та пошук

Endpoint зі списком даних часто стає проблемним не через саму авторизацію, а через неконтрольовані фільтри. Параметри orderby, order, search або meta_key можуть дати змогу отримати більше даних, ніж передбачалося, створити надмірне навантаження або відкрити внутрішні поля.

Не передавайте довільні значення безпосередньо в WP_Query чи власний SQL-запит. Для сортування і статусів використовуйте заздалегідь визначений список. Якщо потрібен SQL, застосовуйте $wpdb->prepare() і не формуйте запит конкатенацією рядків із запиту користувача.

Для великих або дорогих запитів додайте обмеження на page, per_page, довжину пошукового рядка та кількість результатів. За потреби передбачте кешування публічних відповідей, але не змішуйте в одному кеші дані різних користувачів.

  • Обмежуйте per_page і не дозволяйте клієнту довільно збільшувати ліміт.
  • Використовуйте whitelist для orderby та order.
  • Не дозволяйте фільтрувати за службовими meta_key без чіткої потреби.
  • Перевіряйте, чи не повертає endpoint записи зі статусами draft, private або trash.
  • Для чутливих маршрутів розгляньте rate limiting на рівні сервера, проксі або спеціального захисного рішення.

Протестуйте endpoint у різних сценаріях

Перевірка лише в браузері під обліковим записом адміністратора не показує реальний рівень захисту. Один і той самий маршрут потрібно протестувати без авторизації, звичайним користувачем, користувачем з потрібною capability та адміністратором.

Перевіряйте не лише HTTP-код, а й тіло відповіді. Відмова має відбуватися до завантаження зайвих даних, а повідомлення не повинно розкривати внутрішню інформацію. Окремо перевірте спроби доступу до чужого ID, підміну параметрів, великі значення, порожні рядки та неочікувані типи даних.

  • Без входу: приватний endpoint має повертати відмову, а не частковий список.
  • Звичайний користувач: доступ дозволяється лише в межах його capability і власних об’єктів.
  • Користувач з потрібною capability: відповідь містить тільки передбачені поля.
  • Адміністратор: розширений доступ не повинен випадково відкривати службові дані.
  • Чужий ідентифікатор: запит не має повертати об’єкт лише тому, що ID є коректним.
  • Невалідні параметри: endpoint повертає передбачувану помилку, а не PHP warning чи службовий exception.
curl -i https://example.com/wp-json/site/v1/orders

curl -i \
  -H 'X-WP-Nonce: ваш_nonce' \
  'https://example.com/wp-json/site/v1/orders?page=1&status=processing'

# Також перевірте запит із cookie звичайного користувача
# та спробуйте змінити ID на об'єкт іншого користувача.

Типові помилки під час захисту REST API

Найчастіше витоки виникають через кілька повторюваних рішень: дозвіл для всіх заради зручності фронтенду, перевірку лише факту входу або повернення повного результату WordPress-функції. Такі помилки можуть залишатися непомітними, поки endpoint не почнуть активно сканувати.

Захист потрібно закладати на рівні маршруту, конкретної операції та кожного об’єкта. Не варто покладатися на те, що endpoint викликається тільки вашим інтерфейсом: будь-який клієнт може сформувати HTTP-запит самостійно.

  • permission_callback => __return_true для приватних даних.
  • Перевірка is_user_logged_in() без перевірки capability.
  • Перевірка ролі замість capability, через що права стають менш гнучкими.
  • Відсутність перевірки власника ресурсу.
  • Повернення get_users(), WP_Post, повного WC_Order або всіх мета-полів.
  • Використання параметрів запиту без sanitize_callback і validate_callback.
  • Відсутність пагінації, лімітів і whitelist для фільтрів.
  • Спроба захистити endpoint тільки через nonce, CORS, секретний URL або назву маршруту.

Поширені запитання

Чи можна залишити permission_callback як __return_true для GET-запиту?

Так, але лише якщо endpoint справді публічний і кожне поле відповіді призначене для анонімного доступу. Метод GET сам по собі не робить дані безпечними. Для персональної, комерційної або службової інформації потрібна перевірка прав.

Чи достатньо перевірити is_user_logged_in()?

Ні. Ця перевірка показує лише, що користувач автентифікований. Вона не підтверджує право на конкретну дію або об’єкт. Використовуйте current_user_can() і, коли потрібно, перевірку власника ресурсу.

Чи захищає REST API nonce від усіх атак?

Ні. Nonce у cookie-автентифікації допомагає захиститися від CSRF, але не замінює capability, перевірку власника, валідацію параметрів і безпечне формування відповіді. Це один елемент захисту, а не повна авторизація.

Чи безпечно повертати результат get_users() або WP_Post?

Зазвичай ні. Такі об’єкти можуть містити більше полів, ніж потрібно клієнту, зокрема email, службові властивості або метадані. Формуйте явний масив дозволених полів і не передавайте сирі об’єкти.

Який статус відповіді використовувати, якщо користувач не має доступу?

Для неавторизованого запиту зазвичай використовують 401, а для автентифікованого користувача без потрібних прав — 403. Якщо ресурс не має бути розкритий конкретному користувачу, у деяких сценаріях доречний 404, щоб не підтверджувати його існування.

Висновок

Безпечний кастомний REST endpoint — це не лише правильний URL і callback. Потрібно визначити аудиторію маршруту, перевірити capability та право на конкретний об’єкт, провалідувати параметри й повернути тільки мінімально необхідні дані.

Після кожної зміни перевіряйте endpoint без авторизації, під різними ролями та з підміною параметрів. Якщо різниця у відповідях очікувана й контрольована, а не залежить від випадкових умов, ризик витоку даних значно нижчий.

← Усі матеріали WordPress