REST-эндпоинт в WordPress: права, кеш и где ломается
Кастомный эндпоинт с проверкой прав выглядит безопасно, пока кеш не отдаст чужие данные. Разбираем, как WordPress проверяет разрешения и почему обычный объектный кеш здесь не работает.

Сайт на WordPress начал отдавать в REST API приватные поля пользователей всем подряд. Эндпоинт зарегистрирован правильно, permission_callback вызывается, но кешированный ответ игнорирует контекст запроса. Проблема проявилась на втором запросе: первый пользователь получил свои данные, второй — данные первого.
Как WordPress обрабатывает REST-запрос
Когда приходит запрос к /wp-json/myplugin/v1/data, WordPress поднимает весь core, загружает плагины и передаёт управление WP_REST_Server. Сервер ищет зарегистрированный маршрут, проверяет, совпадает ли URL с регулярным выражением маршрута, и вызывает цепочку коллбэков:
- validate_callback — проверяет формат параметров запроса. Если вернёт false или WP_Error, запрос отклоняется с кодом 400.
- permission_callback — проверяет права доступа. Если вернёт false, сервер отдаёт 403. Если вернёт WP_Error с кодом, этот код уйдёт в ответ.
- callback — основная функция эндпоинта. Получает объект WP_REST_Request, возвращает данные или WP_REST_Response.
Порядок жёсткий: permission_callback выполняется после validate, но до основного callback. Это значит, что дорогостоящую операцию — запрос в базу, обращение к API — безопасно делать только в основном коллбэке. Но именно здесь возникает соблазн закешировать результат.
Регистрация эндпоинта с проверкой прав
// WordPress 6.x
add_action('rest_api_init', function() {
register_rest_route('myplugin/v1', '/data', [
'methods' => 'GET',
'callback' => 'myplugin_get_data',
'permission_callback' => 'myplugin_check_permission',
'args' => [
'user_id' => [
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
]);
});
function myplugin_check_permission(WP_REST_Request $request) {
// Проверяем, что текущий пользователь может читать данные
if (!current_user_can('read')) {
return new WP_Error(
'rest_forbidden',
'У вас нет прав для доступа к этому ресурсу.',
['status' => 403]
);
}
return true;
}
function myplugin_get_data(WP_REST_Request $request) {
$user_id = $request->get_param('user_id');
// Запрос в базу — дорого
$data = get_user_meta($user_id, 'private_field', true);
return rest_ensure_response([
'user_id' => $user_id,
'data' => $data
]);
}
Этот код работает: WordPress вызовет permission_callback до того, как выполнится основная функция. Но если добавить кеширование внутри myplugin_get_data, контекст пользователя потеряется.
Почему объектный кеш WordPress не спасёт
WordPress предоставляет wp_cache_get() и wp_cache_set() — функции объектного кеша. По умолчанию это оперативная память процесса PHP, которая живёт в рамках одного запроса. При установке плагина вроде Redis Object Cache или Memcached кеш становится персистентным и доступен между запросами.
Опасный код:
function myplugin_get_data(WP_REST_Request $request) {
$user_id = $request->get_param('user_id');
// Ключ кеша не учитывает текущего пользователя
$cache_key = 'user_data_' . $user_id;
$cached = wp_cache_get($cache_key, 'myplugin');
if ($cached !== false) {
return rest_ensure_response($cached);
}
$data = get_user_meta($user_id, 'private_field', true);
$response = [
'user_id' => $user_id,
'data' => $data
];
wp_cache_set($cache_key, $response, 'myplugin', 300);
return rest_ensure_response($response);
}
Первый пользователь запрашивает /wp-json/myplugin/v1/data?user_id=5. WordPress проверяет его права, выполняет callback, кеширует ответ с ключом user_data_5. Второй пользователь, у которого нет прав видеть данные пользователя 5, запрашивает тот же URL. Permission_callback проверяет только capability текущего пользователя, но не владение запрашиваемым ресурсом. Если проверка пройдена (например, оба пользователя имеют роль subscriber и capability 'read'), кешированный ответ отдаётся без повторного запроса в базу.
Ключ кеша обязан включать идентификатор текущего пользователя, даже если параметры запроса этого не требуют.
Где кешировать безопасно
Стратегии кеширования REST-ответов в WordPress различаются по уровню изоляции и сложности инвалидации:
| Стратегия | Изоляция по пользователю | Где ломается |
|---|---|---|
| Транзиенты с user_id в ключе | Да, если включён в ключ | Инвалидация при изменении данных, рост базы при большом числе пользователей |
| Объектный кеш с составным ключом | Да | Нужен персистентный бэкенд (Redis, Memcached), иначе живёт только в рамках запроса |
| HTTP-заголовки Cache-Control | Нет, кеш на стороне клиента или CDN | Не применимо к приватным данным, только к публичным эндпоинтам |
| Кеш внутри permission_callback | Невозможно | Permission_callback вызывается до основного коллбэка, кешировать результат проверки прав нельзя — контекст меняется |
Безопасный вариант с транзиентами:
function myplugin_get_data(WP_REST_Request $request) {
$user_id = $request->get_param('user_id');
$current_user_id = get_current_user_id();
// Ключ включает и запрашиваемого, и текущего пользователя
$cache_key = "user_data_{$user_id}_for_{$current_user_id}";
$cached = get_transient($cache_key);
if ($cached !== false) {
return rest_ensure_response($cached);
}
$data = get_user_meta($user_id, 'private_field', true);
$response = [
'user_id' => $user_id,
'data' => $data
];
set_transient($cache_key, $response, 300);
return rest_ensure_response($response);
}
Транзиент сохраняется в таблицу wp_options. При большом числе пользователей и частых запросах таблица растёт. Для высоконагруженных API лучше объектный кеш с Redis и периодической очисткой по TTL.
Когда permission_callback возвращает WP_Error
Если вернуть просто false, WordPress отдаст стандартную ошибку 403 без пояснений. Если вернуть WP_Error, можно управлять кодом и сообщением:
function myplugin_check_permission(WP_REST_Request $request) {
$user_id = $request->get_param('user_id');
$current_user_id = get_current_user_id();
// Только владелец или администратор могут читать данные
if ($current_user_id != $user_id && !current_user_can('manage_options')) {
return new WP_Error(
'rest_user_mismatch',
'Вы можете запрашивать только собственные данные.',
['status' => 403]
);
}
return true;
}
WP_Error позволяет различать причины отказа: отсутствие аутентификации, недостаточные права, попытка доступа к чужому ресурсу. Логирование таких ошибок помогает найти атаки перебором чужих user_id.
Где подход ломается
- Concurrent-запросы от одного пользователя. Если два запроса приходят одновременно до того, как первый положил данные в кеш, оба выполнят тяжёлый запрос в базу. Решение — dog-pile protection: временная блокировка ключа на время генерации данных, но WordPress из коробки этого не предоставляет.
- Инвалидация кеша при изменении данных. Если пользователь обновил поле через другой эндпоинт, кеш устарел. Нужно вручную вызывать
delete_transient()в callback, который меняет данные, или хукать экшенupdate_user_meta. - Кеш на уровне HTTP (nginx, Varnish, CDN). Если перед WordPress стоит обратный прокси с кешированием, он не видит содержимое cookie и может отдать приватные данные публично. Для REST API с авторизацией нужно либо исключить маршруты из кеширования, либо настроить Vary: Cookie.
- Большой TTL при частых изменениях данных. Транзиент на 300 секунд подходит для справочных данных, но не для корзины или статуса заказа, которые меняются каждую минуту.
Что смотреть в продакшене
Включи логирование REST-запросов с кодом ответа и user_id. Ищи всплески 403 с одним и тем же user_id в параметрах — это либо атака, либо баг в клиенте. Мерь hit rate кеша: если он ниже 50%, ключи слишком специфичны или TTL слишком короткий. Для транзиентов отслеживай размер таблицы wp_options: если строк с префиксом _transient_ больше нескольких тысяч, нужна периодическая очистка через cron или переход на объектный кеш.
Проверь заголовки ответа: REST API WordPress по умолчанию добавляет X-WP-Total, X-WP-TotalPages для коллекций, но не управляет Cache-Control. Если данные приватные, добавь Cache-Control: private, no-cache в ответ через фильтр rest_post_dispatch.
Проверка грамотности: Орфография, пунктуация, стиль — с объяснением каждой правки
Спросить за 10 ₽Источники: REST API Handbook — WordPress Developer Resources, Function Reference: register_rest_route — WordPress Codex
Частые вопросы
Нужно ли указывать permission_callback, если эндпоинт публичный?
Что будет, если permission_callback вернёт строку или число вместо boolean?
Работает ли этот подход в WordPress 5.x?
Можно ли кешировать результат permission_callback, чтобы не проверять права при каждом запросе?
Почему кешированный ответ отдаётся с кодом 200, даже если данные устарели?
Что ломается первым при росте нагрузки на REST API с кешем?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.