Кастомный REST API-эндпоинт в WordPress удобно использовать для AJAX-запросов, мобильного приложения, личного кабинета или интеграции с другой системой. Однако каждый такой маршрут становится отдельной точкой доступа к данным. Если неправильно настроить права или возвращать лишнюю информацию, эндпоинт может превратиться в источник утечки данных.
Самый опасный сценарий — оставить permission_callback со значением __return_true, а в callback возвращать заказы, записи, email пользователей, служебные метаполя или другие внутренние данные. Ниже рассмотрен практический подход к регистрации, проверке доступа, обработке параметров и тестированию кастомных REST-маршрутов.
Определите, кому действительно нужен доступ
До написания кода определите, должен ли эндпоинт быть публичным, доступным авторизованным пользователям или только отдельной роли. Публичный маршрут оправдан, если все данные в его ответе предназначены для анонимного просмотра: например, список категорий, заголовки опубликованных материалов или общедоступный статус сервиса.
Если эндпоинт возвращает персональные, коммерческие или административные данные, доступ необходимо ограничить. При этом скрытый URL не является защитой: маршрут можно найти в коде JavaScript, документации мобильного приложения, логах или сетевых запросах браузера.
Важно разделять аутентификацию и авторизацию. Аутентификация отвечает на вопрос, кто такой пользователь. Авторизация определяет, имеет ли именно этот пользователь право выполнить конкретную операцию или просмотреть конкретный объект.
- Публичные данные: permission_callback может разрешать доступ без входа.
- Данные авторизованного пользователя: проверяйте наличие входа и доступ к его объектам.
- Административные или финансовые данные: проверяйте соответствующую capability.
- Операции с конкретной записью, заказом или пользователем: дополнительно проверяйте право именно на этот объект.
Настройте permission_callback, а не полагайтесь на callback
В register_rest_route() параметр permission_callback должен быть отдельным барьером перед выполнением основной логики. Не следует сначала получать данные, а затем проверять права внутри callback: это усложняет код и повышает риск ошибки.
Для приватного эндпоинта проверяйте 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');
},
]);
Проверяйте аутентификацию и права на уровне объекта
Проверки роли недостаточно для эндпоинта, который работает с конкретной записью или заказом. Пользователь может иметь право просматривать собственные данные, но не данные другого пользователя. Поэтому после получения идентификатора объекта нужно проверить его принадлежность текущему пользователю или capability для редактирования либо просмотра именно этого объекта.
Для REST API WordPress с cookie-аутентификацией запрос должен содержать корректный nonce, обычно передаваемый в заголовке X-WP-Nonce. Nonce помогает защититься от CSRF, но не заменяет проверку capability. Для интеграций без браузерной сессии используют подходящий способ аутентификации, например Application Passwords или механизм, предусмотренный конкретной системой.
Не считайте CORS или ограничения по User-Agent средством контроля доступа. Они могут влиять на поведение клиента, но не заменяют серверную проверку.
- Получайте текущего пользователя через API WordPress, а не из параметра запроса.
- Сравнивайте владельца ресурса с 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);
},
],
],
]);
Формируйте ответ по принципу минимально необходимых данных
Даже если пользователь имеет право вызывать эндпоинт, это не означает, что ему нужны все поля объекта. Возврат необработанного WP_Post, результата get_users(), полного заказа или массива метаполей может раскрыть email, телефоны, внутренние идентификаторы, служебные флаги и другие данные.
Создавайте явный массив ответа с полями, необходимыми конкретному клиенту. Такой подход одновременно повышает безопасность и стабильность API, а также позволяет лучше контролировать изменения структуры данных.
Особенно осторожно работайте с user_meta, post_meta и данными заказов. Не возвращайте пароли, токены, ключи интеграций или значения, используемые во внутренней логике сайта.
- Определите контракт эндпоинта: какие поля, типы и форматы он возвращает.
- Не передавайте объекты WordPress в ответ без явного преобразования.
- Используйте пагинацию для списков и устанавливайте разумный лимит количества записей.
- Возвращайте понятные коды и сообщения об ошибках без stack trace, SQL-запросов и служебных путей на production.
- Разделяйте административный и публичный эндпоинты, даже если они работают с одним и тем же типом данных.
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() без фільтрації.
Защищайте фильтры, сортировку и поиск
Эндпоинт со списком данных часто становится проблемным не из-за самой авторизации, а из-за неконтролируемых фильтров. Параметры orderby, order, search или meta_key могут позволить получить больше данных, чем предполагалось, создать чрезмерную нагрузку или раскрыть внутренние поля.
Не передавайте произвольные значения напрямую в WP_Query или собственный SQL-запрос. Для сортировки и статусов используйте заранее определённый список. Если требуется SQL, применяйте $wpdb->prepare() и не формируйте запрос конкатенацией строк из пользовательского запроса.
Для больших или ресурсоёмких запросов добавьте ограничения на page, per_page, длину поисковой строки и количество результатов. При необходимости предусмотрите кэширование публичных ответов, но не смешивайте в одном кэше данные разных пользователей.
- Ограничивайте per_page и не позволяйте клиенту произвольно увеличивать лимит.
- Используйте whitelist для orderby и order.
- Не разрешайте фильтрацию по служебным значениям meta_key без чёткой необходимости.
- Проверяйте, не возвращает ли эндпоинт записи со статусами draft, private или trash.
- Для чувствительных маршрутов рассмотрите rate limiting на уровне сервера, прокси или специального защитного решения.
Тестируйте эндпоинт в разных сценариях
Проверка только в браузере под учётной записью администратора не показывает реальный уровень защиты. Один и тот же маршрут нужно протестировать без авторизации, обычным пользователем, пользователем с необходимой capability и администратором.
Проверяйте не только HTTP-код, но и тело ответа. Отказ должен происходить до загрузки лишних данных, а сообщение не должно раскрывать внутреннюю информацию. Отдельно проверьте попытки доступа к чужому ID, подмену параметров, большие значения, пустые строки и неожиданные типы данных.
- Без входа: приватный эндпоинт должен возвращать отказ, а не частичный список.
- Обычный пользователь: доступ разрешается только в пределах его capability и собственных объектов.
- Пользователь с необходимой capability: ответ содержит только предусмотренные поля.
- Администратор: расширенный доступ не должен случайно открывать служебные данные.
- Чужой идентификатор: запрос не должен возвращать объект только потому, что ID является корректным.
- Невалидные параметры: эндпоинт должен возвращать предсказуемую ошибку, а не 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. Такие ошибки могут оставаться незаметными, пока эндпоинт не начнут активно сканировать.
Защиту необходимо закладывать на уровне маршрута, конкретной операции и каждого объекта. Не следует полагаться на то, что эндпоинт вызывается только вашим интерфейсом: любой клиент может самостоятельно сформировать HTTP-запрос.
- permission_callback => __return_true для приватных данных.
- Проверка is_user_logged_in() без проверки capability.
- Проверка роли вместо capability, из-за чего права становятся менее гибкими.
- Отсутствие проверки владельца ресурса.
- Возврат get_users(), WP_Post, полного WC_Order или всех метаполей.
- Использование параметров запроса без sanitize_callback и validate_callback.
- Отсутствие пагинации, лимитов и whitelist для фильтров.
- Попытка защитить эндпоинт только с помощью nonce, CORS, секретного URL или названия маршрута.
Частые вопросы
Можно ли оставить permission_callback со значением __return_true для GET-запроса?
Да, но только если эндпоинт действительно публичный и каждое поле ответа предназначено для анонимного доступа. Сам по себе метод GET не делает данные безопасными. Для персональной, коммерческой или служебной информации необходима проверка прав.
Достаточно ли проверить is_user_logged_in()?
Нет. Эта проверка показывает только, что пользователь аутентифицирован. Она не подтверждает право на конкретное действие или объект. Используйте current_user_can() и, если необходимо, проверяйте владельца ресурса.
Защищает ли nonce REST API от всех атак?
Нет. При cookie-аутентификации nonce помогает защититься от CSRF, но не заменяет проверку capability, владельца, валидацию параметров и безопасное формирование ответа. Это один из элементов защиты, а не полноценная авторизация.
Безопасно ли возвращать результат get_users() или WP_Post?
Обычно нет. Такие объекты могут содержать больше полей, чем нужно клиенту, включая email, служебные свойства или метаданные. Формируйте явный массив разрешённых полей и не передавайте необработанные объекты.
Какой статус ответа использовать, если у пользователя нет доступа?
Для неаутентифицированного запроса обычно используется 401, а для аутентифицированного пользователя без необходимых прав — 403. Если ресурс не должен быть раскрыт конкретному пользователю, в некоторых сценариях уместен 404, чтобы не подтверждать его существование.
Вывод
Безопасный кастомный REST-эндпоинт — это не только правильный URL и callback. Нужно определить аудиторию маршрута, проверить capability и право на конкретный объект, провалидировать параметры и вернуть только минимально необходимые данные.
После каждого изменения проверяйте эндпоинт без авторизации, под разными ролями и с подменой параметров. Если различия в ответах ожидаемы и контролируются, а не зависят от случайных условий, риск утечки данных значительно ниже.