DarkRiDDeR12 мин

CORS error у cookie API: диагностика без отключения CSRF

БезопасностьHTTP

После выката frontend на новый host интерфейс не получает данные или mutation заканчивается ошибкой. Самое рискованное действие — сделать CORS глобально permissive или выключить CSRF «для проверки». Такой change расширит доступ без review. Диагностика начинается с request contract, не с флага middleware. Цена ошибки — дать origin доступ к данным или mutation.

Маршрут разделяет CORS error, OPTIONS failure и CSRF rejection. Локальный fixture не изображает сеть: его inputs — заданные labels, выход — решения учебного контракта. Он не сообщает response proxy, cookie delivery или access log. Для этого нужен реальный browser evidence в контролируемой среде, без переноса production cookie и secrets в заметку.

Соберите факты до первого исправления

Начните с пяти значений: полный origin страницы, URL target, method, content type и имена request headers. Потом добавьте status и response headers, которые видны в browser DevTools или на boundary proxy, а также application reason, если он не раскрывает token. Разница между https://app.example.test и https://app.example.test:8443 существенна; разница между POST form и PATCH JSON тоже существенна. Лог «CORS failed» без этих полей — не доказательство причины.

Не переиспользуйте пользовательскую production cookie в диагностике. Для реального browser check создайте разрешённую test session, заранее определите тестовую запись и ожидаемый безопасный side effect, а затем удалите или изолируйте данные согласно правилам среды. Если такой стенд пока не готов, зафиксируйте contract review и in-memory fixture как подготовку, но не пишите, что CORS или CSRF уже проверены. Отсутствие evidence — полезный результат: он показывает, какой артефакт нужен до релиза.

Симптом → причина → проверка → действие
СимптомВероятная причинаПроверкаПервое действие
JS не читает responseorigin отсутствует в CORS allow-listсверить full tuple с exact response headerдобавить только нужный origin
Credentials response заблокированwildcard или нет Allow-Credentials: trueсмотреть пару response headers вместевернуть exact origin, не расширять wildcard
OPTIONS не проходитmethod или custom header не разрешёнсверить declared method/header с preflight responseразрешить узкий нужный набор
POST form получил 403CSRF token отсутствует или не совпалпроверить server reason до mutationпочинить выдачу/передачу token
Mutation прошёл, UI увидел errorCORS visibility и server action различаютсясверить server evidence и response policyисправить CORS без снятия CSRF
403 после token checkу пользователя нет business permissionразделить reason CSRF и authorizationчинить policy ресурса, не token

Не выводите server action из console message

CORS error сообщает о том, что browser code не получил ожидаемый cross-origin response по правилам модели. Он не является универсальным сообщением «сервер не видел запрос». Для некоторых request shape сервер мог уже принять HTTP traffic, а JavaScript всё ещё не сможет прочитать representation. Для других, требующих preflight, actual request может не начаться после отказа OPTIONS. Эти варианты нельзя различить по одной строке консоли, поэтому в проверке должны быть и browser evidence, и proxy/application log с безопасным корреляционным идентификатором.

Не делайте из этого аргумент «CORS бесполезен». Он ограничивает, какие foreign origin получают доступ к данным через browser API, и этим уменьшает поверхность чтения. Просто у него другая ответственность, чем у CSRF. Для cookie-backed mutation сервер должен решить, почему request разрешён: token совпал с привязанной к сессии записью, origin прошёл узкую policy, а пользователь имеет нужное право. CORS response header не содержит это доказательство и не должен заменять его в code review.

Маршрут диагностики начинает с симптома и сбора origin, method, headers и server status. Затем он разделяет untrusted origin, credentials response, preflight failure и CSRF 403, у каждого — отдельная проверка и действие.
Схема — чек-лист для разбора evidence. Она не является реальным trace, не утверждает, что browser отправил OPTIONS, и не показывает настоящие cookie или данные пользователя.

Разберите credentials и preflight по отдельности

Если endpoint действительно нужен cookie-authenticated frontend на другом origin, проверить надо две пары значений. Первая — client credentials mode и server Access-Control-Allow-Credentials: true. Вторая — exact source origin и Access-Control-Allow-Origin. Для credentials wildcard запрещён исторической CORS Recommendation. Нельзя исправить одну половину и считать contract готовым: response может всё ещё быть скрыт от JavaScript, а добавление произвольного origin создаст новый trusted reader без явной причины.

Для preflight соберите actual method и header names из client code, затем сверяйте их с узким OPTIONS contract. Например, X-CSRF-Token может требовать разрешения как custom header, а PATCH — как method. Не отвечайте «все методы, все headers» ради скорости: это затрудняет review и превращает следующую ошибку клиента в неявно допустимый API path. Когда endpoint поддерживает form POST, обязательно выполните отдельный CSRF test, потому что form-shaped request не обязан проходить через тот же preflight путь.

Проверьте терминологию локально, не подменяя стенд

В скрипте рядом со статьёй fixture держит несколько отрицательных веток: credentialed wildcard отклоняется договором; port 8443 создаёт другой origin; same-origin POST без token всё равно получает deny-token; form-shaped POST тоже не проходит серверное CSRF-условие. Эти assertions полезны перед рефакторингом middleware, потому что запрещают склеить CORS и CSRF в одну переменную. Но они не запускают gateway, framework, browser или HTTP server.

node web/scripts/upgrade-2023-03.mjs --verify-fixture

# PASS означает только следующее:
# входные labels прошли 12 assertions in-memory contract.
# Команда не открывала сеть, не вызывала Fetch, не посылала cookie,
# не создавала OPTIONS и не проверяла реальный API или браузер.

После PASS не пишите «сетевой repro завершён». Верная запись короче: «in-memory contract подтвердил 12 утверждений о заданных labels; реальный browser/API evidence требуется отдельно». Такой текст не выглядит слабее; он показывает, где заканчиваются данные. В качестве следующего artifact приложите к PR скрин или HAR из test environment, server status, выбранный exact origin и отрицательный case без token. Секреты, сами token values и production account identifiers в evidence не нужны.

CSRF-проверка должна пережить CORS-исправление

Для token-based flow найдите место, где сервер выдаёт token, место передачи и точку сравнения до side effect. Если frontend получает token из HTML, проверьте, что route, который рендерит страницу, не кэширует чужую сессию. Если token передаётся custom header, проверьте его имя в narrow preflight allow-list. Если framework уже даёт CSRF middleware, сначала изучите его contract и тесты вместо замены собственной краткой функцией. Самодельная проверка чаще всего забывает rotation, logout, error handling или исключения route.

Origin check удобен как дополнительная защита, но его политика должна быть строгой. Не сравнивайте endsWith('example.test'), не делайте null trusted по умолчанию и не принимайте отсутствующий header молча, если это не описанная compatibility ветка. Если API принимает webhooks, native clients или service-to-service traffic, не пытайтесь заставить их выглядеть как browser CSRF flow. У каждого типа caller должен быть свой явный authentication contract, а исключение из CSRF middleware — ограниченный route, а не отключение на всём приложении.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Зафиксируйте одну failing операцию и её цену: UI не читает данные, mutation отклонён или action мог выполниться без видимого response. Не объединяйте несколько endpoints в один кейс.
  2. Причина. Отделите origin tuple, CORS response, request shape/preflight, CSRF proof и business permission. У каждого пункта должен быть свой owner и log source.
  3. Проверка evidence. В test environment сопоставьте DevTools или HAR с proxy/application status. Если actual request неизвестен, запишите это неизвестное, а не вывод из console text.
  4. Проверка контракта. Запустите fixture и прочитайте отрицательные assertions. Он защищает только смысл статейного договора, не сеть и не session implementation.
  5. Действие. Добавьте exact origin, один method/header в OPTIONS response либо недостающую передачу token. После change повторите только исходную операцию и её отрицательную ветку.
  6. Закрепление. Оставьте автоматический route test на missing/mismatch token и reviewable CORS config. Для нового frontend origin потребуйте отдельный security review, а не копирование существующей строки.

Ограничения и критерий готовности

Этот маршрут не заменяет penetration test, cookie audit, CSP review, XSS defense или authorization test. XSS особенно меняет картину: script на доверенном origin может использовать доступные ему token и API, поэтому CORS и CSRF не являются защитой от выполнения чужого JavaScript в собственном origin. Статья также не назначает TTL token, набор SameSite attributes, format error response или универсальный список trusted origins. Эти параметры зависят от framework, browser support, session model и threat model.

Критерий готовности для одного endpoint узкий и проверяемый: reviewer видит точный source origin, разрешённый credentialed CORS response, нужный preflight contract при наличии custom header, server-side rejection без CSRF proof и отдельную permission check. Если есть только CORS header, работа не готова. Если есть только token test, frontend может не прочитать expected response. Если есть только fixture PASS, нет evidence реальной интеграции. Соберите все три слоя, но не объявляйте один из них заменой другого.

Историческая граница марта 2023

Нормативные ссылки этой партии датированы до конца марта 2023: RFC 6454 опубликован в 2011 году, W3C CORS Recommendation — в 2014 году, а WHATWG Fetch snapshot — 24 марта 2023 года. Они достаточны, чтобы проверить модель origin, CORS credentials и preflight. Конфигурацию конкретного proxy, browser quirks и middleware version всё равно нужно сверять в документации используемой платформы и в test environment.

Проверяемые источники