Когда фронтенд работает на отдельном домене, браузер применяет политику same-origin. Даже если endpoint WordPress REST API открыт и отлично отвечает через curl или Postman, запрос из JavaScript может быть остановлен браузером из-за CORS.
Задача решается не отключением защиты и не универсальным Access-Control-Allow-Origin: *, а точной настройкой разрешённых origins, методов, заголовков и способа аутентификации. Важно понимать: CORS — это браузерный механизм контроля cross-origin запросов, а не система авторизации WordPress.
Как WordPress обрабатывает CORS по умолчанию
В ядре WordPress есть функция rest_send_cors_headers(). Она подключена к REST API через стандартные фильтры и отвечает за CORS-заголовки. Когда запрос содержит Origin, WordPress может вернуть Access-Control-Allow-Origin, разрешённые методы, credentials и Vary: Origin.
Это означает, что перед добавлением собственных заголовков через Nginx, Cloudflare или PHP нужно проверить, что уже отдаёт WordPress. Дублирующиеся или противоречивые CORS-заголовки часто создают проблему вместо её решения.
Почему запрос проходит в Postman, но падает в браузере
Postman и серверные HTTP-клиенты не обязаны применять браузерную CORS-политику. Поэтому ответ 200 в таком клиенте ещё не доказывает, что запрос допустит браузер.
Проверяйте три вещи отдельно:
- доступен ли endpoint и возвращает ли ожидаемый HTTP-код;
- есть ли корректные CORS-заголовки для конкретного Origin;
- проходит ли preflight OPTIONS для методов и заголовков, которые собирается отправить фронтенд.
Что такое Origin
Origin включает схему, хост и порт. Для браузера https://app.example.com, https://www.example.com и http://app.example.com — разные origins.
Если приложение должно работать только с одного или нескольких известных адресов, лучше проверять их по белому списку. Не стоит отражать любой входящий Origin без валидации, особенно если endpoint работает с авторизованными данными.
Почему wildcard не подходит для credentials
Распространённая попытка исправления выглядит так:
Access-Control-Allow-Origin: *
Для публичного read-only API это иногда допустимо, но для запросов с cookies или credentials такой подход не подходит. Если браузер должен передавать авторизационные данные, серверу нужно вернуть конкретный разрешённый Origin и корректно обработать credentials.
Дополнительно важно отправлять Vary: Origin, чтобы reverse proxy или CDN не закэшировал ответ с заголовком для одного домена и не отдал его другому.
Preflight OPTIONS
Перед некоторыми cross-origin запросами браузер отправляет preflight: HTTP OPTIONS с заголовками Origin, Access-Control-Request-Method и иногда Access-Control-Request-Headers. Браузер проверяет, разрешает ли сервер будущий основной запрос.
Preflight часто появляется при POST/PUT/PATCH/DELETE, использовании Authorization, нестандартных заголовков или некоторых Content-Type.
WordPress REST API содержит обработку OPTIONS, но её могут нарушить WAF, reverse proxy, CDN, security plugin или кастомные правила сервера. Поэтому при ошибке сначала посмотрите фактический ответ OPTIONS, а не только JavaScript console.
Разрешённые request headers
WordPress предоставляет фильтр rest_allowed_cors_headers. Через него можно скорректировать набор заголовков, которые REST API разрешит в CORS-сценарии.
add_filter( 'rest_allowed_cors_headers', function ( $headers ) {
$headers[] = 'Authorization';
$headers[] = 'X-App-Version';
return array_values( array_unique( $headers ) );
} );
Не добавляйте заголовки «на всякий случай». Список должен соответствовать реальным запросам приложения.
Как ограничить конкретные origins
Если нужен строгий контроль, удобнее реализовать его в одном месте: либо на уровне приложения, либо reverse proxy, но не распределять частичную логику между WordPress, Nginx и CDN.
Пример идеи для WordPress-плагина:
add_filter( 'rest_pre_serve_request', function ( $served ) {
$origin = get_http_origin();
$allowed = [
'https://app.example.com',
'https://admin.example.com',
];
if ( $origin && in_array( $origin, $allowed, true ) ) {
header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
header( 'Access-Control-Allow-Credentials: true' );
header( 'Vary: Origin', false );
}
return $served;
} );
На практике нужно учитывать уже отправленные ядром заголовки, чтобы не получить два значения Access-Control-Allow-Origin. Поэтому production-реализацию следует строить после анализа текущего ответа сайта и используемой инфраструктуры.
CORS не заменяет permission_callback
Даже идеально настроенный CORS не защищает REST route от неавторизованного серверного запроса. Каждая кастомная REST route должна иметь корректный permission_callback и проверять права пользователя или другой механизм доступа.
Если endpoint меняет заказы, клиентов, настройки или другой чувствительный ресурс, нельзя считать проверку Origin полноценной авторизацией. Origin помогает браузеру контролировать cross-site доступ, но сервер должен самостоятельно решать, имеет ли вызывающая сторона право выполнить операцию.
Cookie authentication и nonce
Для запросов внутри обычной WordPress-сессии REST API использует cookie authentication. Официальная документация WordPress указывает, что REST-запросы из WordPress должны передавать nonce, обычно через X-WP-Nonce.
Это удобно для интерфейса внутри WordPress, но внешний SPA на другом домене требует аккуратной настройки cookies, SameSite, HTTPS, credentials и CORS. Если архитектура позволяет, зачастую безопаснее не передавать административную WordPress cookie напрямую внешнему браузерному приложению.
Application Passwords для внешних интеграций
Для программного доступа WordPress поддерживает Application Passwords. Они предназначены для API-аутентификации и должны использоваться только по HTTPS. Подробнее — в документации REST API Authentication.
Application Password не стоит встраивать в публичный JavaScript bundle: пользователь сможет увидеть секрет. Для SPA лучше использовать собственный backend/BFF, который хранит серверные credentials и обращается к WordPress от имени приложения.
Практику Application Passwords мы отдельно разбирали в материале WordPress REST API Application Passwords.
Пример архитектуры для headless WordPress
Для публичного каталога или контента схема может быть простой:
- frontend запрашивает публичные GET endpoints;
- WordPress разрешает только нужные origins либо API остаётся публичным для чтения;
- изменяющие операции идут через backend приложения;
- секреты хранятся только на сервере;
- CDN кэширует публичный контент с корректным учётом Origin/Vary.
Для личного кабинета схема сложнее: нужно проектировать сессии, CSRF-защиту, refresh/token flow или backend proxy. Простое добавление CORS-заголовков эту задачу не решает.
Cloudflare, Nginx и reverse proxy
Даже если WordPress отвечает правильно, промежуточный слой может изменить результат. Проверьте:
- не блокируется ли метод OPTIONS;
- доходит ли заголовок Origin до WordPress;
- не удаляется ли Authorization;
- не добавляет ли прокси второй Access-Control-Allow-Origin;
- сохраняется ли Vary: Origin;
- не кэшируются ли preflight-ответы с неверной политикой.
Если CORS реализован на edge-уровне, WordPress не должен одновременно формировать конфликтующую политику.
Как диагностировать CORS через curl
Сначала проверьте preflight:
curl -i -X OPTIONS 'https://example.com/wp-json/wp/v2/posts' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: Authorization, Content-Type'
Затем основной запрос:
curl -i 'https://example.com/wp-json/wp/v2/posts' \
-H 'Origin: https://app.example.com'
Повторите тест с разрешённым и запрещённым Origin. Смотрите не только HTTP-код, но и фактические Access-Control-* и Vary.
Типовые ошибки
- Два Access-Control-Allow-Origin. Один добавляет WordPress, второй Nginx или CDN.
- Wildcard плюс credentials. Браузер не принимает такую комбинацию.
- OPTIONS блокируется WAF. Основной endpoint работает, но браузер до него не доходит.
- Authorization отсутствует в allowed headers. Preflight завершается ошибкой.
- Секрет находится во frontend. CORS не скрывает Application Password или API key от пользователя.
- Нет permission_callback. Разрешённый Origin ошибочно воспринимается как контроль доступа.
- CDN не учитывает Vary. Клиенты получают заголовок, закэшированный для другого Origin.
Чек-лист безопасной настройки
- Зафиксировать список frontend origins.
- Определить публичные и авторизованные endpoints.
- Выбрать способ аутентификации отдельно от CORS.
- Проверить стандартные CORS-заголовки WordPress.
- Разрешить только реально используемые methods и headers.
- Проверить OPTIONS на всех промежуточных слоях.
- Проверить permission_callback и capabilities.
- Не хранить серверные API-секреты в JavaScript.
- Проверить кэш и Vary: Origin.
- Протестировать разрешённый и запрещённый Origin.
Для сложных интеграций важнее не «убрать ошибку CORS», а построить предсказуемую границу между WordPress и внешним приложением. A.S Groups занимается интеграциями WordPress через REST API и webhooks, настройкой авторизации и серверной логики. Если внешний фронтенд уже есть, можно прислать схему доменов, endpoints и текущую ошибку для диагностики.
Обсуждение
Вопросы и комментарии
Можно уточнить детали статьи или поделиться своим опытом. Первый комментарий проходит проверку.