Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → CORS и безопасность браузера: практика

CORS без магии: как браузер решает, что можно, и как это настроить

Как на самом деле работает CORS: same-origin, preflight, креденшелы и почему Access-Control-Allow-Origin: * с куками несовместим; заголовки, частые ошибки, чек-лист.

Прямой ответ

CORS — не настройка сервера «разрешить запросы», а механизм браузера: сервер лишь объявляет заголовками, каким origins он доверяет, а решение принимает браузер (curl/requests/httpx CORS не существует вообще — httpx-гайд). Три факта, снимающих 90% путаницы: запрос отправляется всегда — CORS блокирует не отправку, а чтение ответа скриптом (поэтому «запрос прошёл, а фронт ругается»); «простые» запросы (GET/POST с базовыми заголовками) уходят без preflight, остальные (JSON-тело с Content-Type: application/json, PUT/DELETE, кастомные заголовки — то есть почти весь современный API) предваряются OPTIONS-preflight'ом, и Access-Control-Allow-Origin: * несовместим с credentials (куки не поедут — нужен точный origin + Allow-Credentials: true). Правильная настройка — белый список origins из конфигурации (никаких «отражать любой Origin» и никаких null в проде), а «починить CORS» на сервере, когда фронт и API должны быть одним origin'ом, — через reverse-proxy (nginx), а не через раскручивание разрешений.

Модель: same-origin, простой запрос, preflight

Правило по умолчанию — same-origin policy: скрипт со страницы свободно читает ответы того же origin (схема+хост+порт), остальное — только с явного разрешения сервера. **Простой запрос** (GET/HEAD/POST, заголовки из безопасного набора, Content-Type только form/multipart/text) — уходит сразу, браузер проверяетAccess-Control-Allow-Originв ответе. **Preflight** — браузер шлёт OPTIONS сAccess-Control-Request-Method/Request-Headers, сервер отвечает разрешающими заголовками (Allow-Methods, Allow-Headers, Max-Age для кеша preflight'а), затем реальный запрос: каждый новый заголовок авторизации (Authorization) или метод (PATCH) — это preflight, и если OPTIONS не обработан (404/405 от роутера) — «CORS-ошибка» на ровном месте. Отдельный класс — **credentialed-запросы** (credentials: 'include', куки): требуют точного origin в Allow-Origin (не звёздочку), Allow-Credentials: true`, и SameSite-политики кук — связка из четырёх совпадений, где любое несоблюдение даёт отказ без внятного объяснения.

Частые ошибки: симптомы и лечение

«No 'Access-Control-Allow-Origin' header» — middleware CORS не подключён или origin не в списке; лечение: белый список в конфиге (FastAPI CORSMiddleware с явным allow_origins=[...]), проверка по точному совпадению (схема и порт входят в origin — ≠). «Response to preflight request doesn't pass» — OPTIONS не достигает обработчика (роутер не знает OPTIONS, или nginx/фаервол режет метод); лечение: middleware до роутинга, проверка через curl -X OPTIONS -H "Origin: " -H "Access-Control-Request-Method: POST" -i. «Credentials mode 'include' + wildcard» — звёздочка с куками; лечение: точный origin + credentials, либо отказ от кук в пользу заголовка Authorization (для API — JWT-поток без кук вообще не требует CORS-креденшелов). «Отражаем Origin как есть» (Allow-Origin: $http_origin) — это * с иллюзией контроля, плюс кеш-отравление CDN (ответ с одним origin закеширован и отдан другому); лечение: только белый список. «CORS в проде после локалки» — origin'ы differ по окружению: список из переменной окружения, а не хардкод.

Когда CORS вообще не нужен

Три ситуации, где проблема растворяется архитектурно. Один origin: фронт и API под одним хостом (SPA раздаёт тот же nginx, что проксирует /api) — same-origin, CORS не возникает; самый частый «правильный ответ» вместо настройки заголовков. SSR/BFF: запросы к API делает сервер (Node/Python), браузер получает готовую страницу — server-to-server без CORS; BFF-паттерн заодно прячет внутренние сервисы. Публичный API без кук: авторизация заголовком, звёздочка или явные origins — минимальная настройка. И наоборот, ситуация, где CORS-ослабление — признак проблемы: «внутренний API доступен из браузера с любого сайта» — это не CORS-настройка, а отсутствующая авторизация (CORS не защищает сервер от не-браузерных клиентов — curl игнорирует его полностью; защита API — авторизация и rate limit, CORS — про доверие браузерных скриптов).

Практический сценарий: диагностика за пять минут

Симптом: фронт на API на PATCH с Authorization — «blocked by CORS policy». Диагностика: (1) DevTools Network — найти OPTIONS-запрос: статус? 404/405 — preflight не обработан (middleware CORS не подключён до роутера или nginx не пропускает OPTIONS); 200 — смотреть его заголовки ответа. (2) В заголовках preflight: есть Access-Control-Allow-Headers: authorization? нет — добавить в middleware (preflight спрашивает именно те заголовки, что пойдут в реальном запросе). (3) Allow-Methods содержит PATCH? (4) Реальный запрос — Allow-Origin точно (схемой и без слеша в конце)? (5) Кеш preflight'а (Max-Age`) — после фикса конфигурации браузер может держать старый отказ: жёсткая перезагрузка. В девяти случаях из десяти проблема в одном из заголовков preflight'а, а не в «сервере не разрешает CORS»; curl-проверка OPTIONS-запроса показывает серверную правду без браузера. Постоянная профилактика: CORS-конфиг в коде с origins из конфигурации + интеграционный тест «preflight с тестовым origin проходит» — конфиг живёт с приложением, а не в памяти админа.

FAQ

Почему CORS-ошибка появляется только в браузере, а Postman работает? Потому что CORS существует только в браузере: Postman/curl/httpx — не браузеры, same-origin policy к ним не применяется и заголовки Allow-Origin они не проверяют. Это же объясняет, почему «у бэкендера всё работает»: серверная проверка CORS бессмысленна, проверять надо именно браузерным клиентом или эмулировать его заголовки Origin в curl.

CORS защищает сервер? Нет — он управляет тем, какие браузерные скрипты могут читать ответы; не-браузерный клиент (curl, сервер) CORS игнорирует. Защита сервера — авторизация, валидация, лимиты; CORS — механизм доверия между origins в браузере.

Почему без CORS «небезопасно», если запрос всё равно уходит? Потому что куки идут автоматически: без CORS злонамеренная страница могла бы читать ответы API, где пользователь аутентифицирован кукой (классическая кража данных). CORS+SameSite закрывают чтение и управление; POST-запрос без чтения ответа (простая форма) исторически разрешён — отсюда CSRF как отдельная забота.

Нужен ли CORS для мобильного приложения? Нет: нативное приложение — не браузер, same-origin policy отсутствует; CORS — исключительно браузерный механизм (вебвью с web-контентом — исключение: там действуют браузерные правила).

Как разрешить локальную разработку без дыр в проде? Список origins из переменной окружения: в dev-конфиге ` в проде — только боевые домены; middleware читает конфигурацию (env-подход), и «открытый CORS» физически не может уехать в прод.

Источники: Fetch Standard (CORS), MDN, документация FastAPI CORSMiddleware — ссылки в шапке.