{ "index": 174, "slug": "editorial-2023-03-practice-csrf-cors", "title": "Cookie API и виджет: как не перепутать CORS с CSRF", "excerpt": "Разберите CORS и CSRF по отдельности: один механизм управляет доступом JavaScript к ответу, другой решает на сервере, можно ли принять изменение данных.", "contentHtml": "
Виджет на https://app.example.test вызывает API на https://api.example.test. После релиза в DevTools появляется CORS error. Пользователь не видит профиль, а команда предлагает поставить Access-Control-Allow-Origin: *. Если API использует cookie, это не исправление. Браузер всё равно скроет credentialed response, а попытка отключить CSRF-проверку может открыть изменение данных с чужой страницы.
Цена ошибки двойная. Рабочий интерфейс перестаёт получать данные. Одновременно сервер может начать принимать state-changing запрос только по cookie. Тогда злоумышленник не обязан читать ответ: ему достаточно заставить браузер жертвы отправить перевод, сменить адрес или удалить запись.
\nТезис: CORS и CSRF отвечают на разные вопросы. CORS определяет, получит ли JavaScript cross-origin доступ к response. CSRF-защита проверяет на сервере, действительно ли запрос на изменение состояния пришёл из разрешённого сценария. Исправляйте эти контуры раздельно.
\nСначала браузер определяет origin. Это комбинация scheme, host и port. Для https://app.example.test и https://api.example.test host различается, поэтому запрос cross-origin. Путь и query в origin не входят. Общий registrable domain тоже не делает два приложения одним origin.
Клиент может запросить credentials: например, передать cookie через fetch(url, { credentials: 'include' }). Это только намерение клиента. Сервер должен вернуть точный Access-Control-Allow-Origin для разрешённого origin и Access-Control-Allow-Credentials: true, если браузер должен открыть response JavaScript-коду. Wildcard * не совместим с credentialed CORS.
CORS не является authorization. Успешная проверка CORS не означает, что пользователь вошёл, имеет право менять конкретный ресурс или передал CSRF-доказательство. Сервер должен выполнить аутентификацию и authorization независимо от CORS.
\nCSRF появляется потому, что браузер может приложить cookie к cross-site запросу. Простая HTML-форма способна отправить POST с application/x-www-form-urlencoded без доступа к ответу и без preflight. Если endpoint меняет состояние только по cookie, такой запрос опасен.
Для stateful API сервер обычно хранит CSRF-token в сессии и требует его в form field или custom header. Обработчик сравнивает token до mutation. Отсутствующий или неверный token даёт отказ. Origin или Referer check может добавить защиту, но не заменяет token, authorization и проверку бизнес-прав.
\nНиже учебный пример политики. Он не открывает сеть, не создаёт cookie, не запускает браузер и не доказывает поведение конкретного production API. В нём показаны две независимые проверки: CORS для чтения ответа и CSRF для изменения состояния.
\nconst corsOrigins = new Set(['https://app.example.test']);\nconst csrfOrigins = new Set([\n 'https://app.example.test',\n 'https://api.example.test',\n]);\n\nfunction checkRequest({ origin, credentials, method, csrfToken }) {\n const cors = corsOrigins.has(origin) &&\n (!credentials || origin !== '*');\n\n const safeMethod = new Set(['GET', 'HEAD', 'OPTIONS']).has(method);\n const csrf = safeMethod ||\n (csrfOrigins.has(origin) && csrfToken === 'token-from-session');\n\n return { cors: cors ? 'allow' : 'deny', csrf: csrf ? 'allow' : 'deny' };\n}\n\n// Учебные проверки, не production-конфигурация:\ncheckRequest({\n origin: 'https://app.example.test',\n credentials: true,\n method: 'POST',\n csrfToken: 'token-from-session',\n}); // { cors: 'allow', csrf: 'allow' }\n\ncheckRequest({\n origin: 'https://evil.example',\n credentials: true,\n method: 'POST',\n csrfToken: undefined,\n}); // { cors: 'deny', csrf: 'deny' }\nСтрока token-from-session здесь условна. В реальном приложении token должен быть непредсказуемым, связанным с сессией и сгенерированным безопасным источником случайности. Сравнение выполняет сервер до побочного эффекта. Не переносите этот код в middleware без проверки cookie policy, proxy и framework-контракта.
Preflight полезен для диагностики формы запроса. Custom header вроде X-CSRF-Token или нестандартный content type часто вызывает OPTIONS-проверку. Но preflight не подтверждает token, сессию и право пользователя. Он проверяет, разрешает ли CORS-политика указанный origin, method и header. Поэтому нельзя считать preflight самостоятельной CSRF-защитой.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| В консоли CORS error при cookie-запросе | Wildcard или отсутствует точный origin | Сверить Origin, Access-Control-Allow-Origin, credentials и Vary: Origin | Оставить allow-list точных origin; не подставлять любое входное значение |
| OPTIONS проходит, POST меняет данные без token | Preflight ошибочно приняли за CSRF-защиту | Отправить form-shaped POST без custom header и проверить серверный ответ | Проверять CSRF-token до mutation для всех state-changing методов |
| 403 после добавления token | Token не связан с текущей сессией или не доходит через proxy | Проверить источник token, cookie, заголовок, нормализацию и точку отказа | Исправить передачу и проверку; не отключать middleware целиком |
| Данные не читаются, но запись всё равно меняется | CORS скрывает response, но сервер принимает запрос по cookie | Смотреть server log и статус actual request отдельно от сообщения браузера | Добавить серверный CSRF-контроль и authorization |
Эта схема предполагает cookie-based authentication и браузерный клиент. Она не описывает OAuth bearer token в заголовке, webhook, native app или server-to-server вызов. Для таких клиентов модель угроз и способ доказать полномочия будут другими.
\nSameSite помогает ограничить отправку cookie, но не отменяет серверную проверку. Его результат зависит от атрибутов cookie, браузера, контекста навигации и окружения. Origin может отсутствовать или иметь значение null. Proxy может изменить набор видимых заголовков. Эти случаи нужно включить в отдельную политику, а не молча считать безопасными.
XSS в доверенном origin может обойти многие CSRF-меры, потому что вредоносный код действует внутри разрешённого контекста. Поэтому исправление CSRF не заменяет защиту от XSS, управление cookie и контроль прав.
\nСценарий готов, если команда может предъявить для одного state-changing endpoint четыре независимых доказательства: точный allow-list origin, ожидаемый CORS response, проверку CSRF-token до mutation и отказ при чужом origin или неверном token. В реальном browser test легитимный запрос читает response и меняет только разрешённый ресурс. Отрицательные запросы получают 403 или эквивалентный отказ, а состояние не меняется. Ни один из этих результатов нельзя заменять одним сообщением CORS в консоли.
\n