CORS от А до Я: история, ошибки и грамотная настройка
Оглавление статьи

CORS (Cross-Origin Resource Sharing) — это механизм браузера и HTTP-заголовков, который позволяет серверу сообщить, каким другим origin разрешено читать его ответы из JavaScript. Именно CORS участвует в ситуации, когда фронтенд открыт на https://app.example.com, а fetch() обращается к на https://api.example.com.
Если настройка не совпадает с запросом, в DevTools появляется что-то вроде blocked by CORS policy или No 'Access-Control-Allow-Origin' header. Исправлять это почти всегда нужно на сервере, reverse proxy или API gateway — не отключением защиты в браузере.
Самая важная мысль: CORS не отвечает на вопрос «можно ли вообще отправить HTTP-запрос этому API». Он определяет, разрешит ли браузер JavaScript страницы другого origin получить доступ к ответу. Поэтому Postman или curl могут работать, когда тот же запрос из браузера падает с CORS error.
CORS error: что проверить в первую очередь
| Ошибка в консоли | Частая причина | Где исправлять |
|---|---|---|
No 'Access-Control-Allow-Origin' header | Сервер не вернул разрешённый origin | Backend / Nginx / API gateway |
... has been blocked by CORS policy | Один из CORS-заголовков не соответствует запросу | Сначала смотреть Network → OPTIONS/response |
preflight request did not succeed | OPTIONS не дошёл до приложения или завершился ошибкой | Proxy, auth middleware, router, backend |
Request header field ... is not allowed | Запрошенный header отсутствует в разрешённых | Access-Control-Allow-Headers |
Method ... is not allowed | Метод не указан среди разрешённых | Access-Control-Allow-Methods |
Credential is not supported if ACAO is '*' | Credentials используются вместе с wildcard origin | Указать конкретный origin |
Mixed Content | HTTPS-страница запрашивает HTTP-ресурс | Это не CORS: перевести API на HTTPS |
origin 'null' | Запуск из file://, sandboxed iframe или другого opaque origin | Изменить способ запуска или явно продумать политику |
Если нужен быстрый алгоритм: откройте DevTools → Network, найдите запрос, проверьте его Origin, затем ответ сервера и, если есть, отдельный запрос OPTIONS. Это быстрее, чем случайно добавлять заголовки по одному.
Что такое origin
Origin определяется тремя частями URL:
scheme —
httpилиhttps;host — например,
example.com;port — например,
443или8080.
Путь не входит в origin.
| URL относительно https://example.com | Тот же origin? | Почему |
|---|---|---|
https://example.com/api | Да | Изменился только path |
https://example.com/users | Да | Path не влияет на origin |
http://example.com | Нет | Другой scheme |
https://api.example.com | Нет | Другой host |
https://example.com:8443 | Нет | Другой port |
Поэтому даже frontend и backend одной компании могут быть cross-origin, если находятся на разных поддоменах или портах.
Зачем появилась Same-Origin Policy
Без ограничений вредоносная страница могла бы попытаться использовать браузер пользователя для чтения данных другого сайта, где пользователь уже авторизован.
Same-Origin Policy (SOP) изолирует документы и скрипты разных origins. Концепция появилась ещё в ранних браузерах эпохи Netscape и со временем стала одним из фундаментальных механизмов безопасности веб-платформы.
CORS появился не как замена SOP, а как контролируемый способ ослабить её ограничения для HTTP-доступа: сервер сам сообщает браузеру, какие внешние origins могут читать его ресурсы.
SOP: по умолчанию не доверяй другому origin. CORS: сервер может явно разрешить определённое cross-origin взаимодействие.
Как работает CORS
Представим frontend:
https://app.example.com
и API:
https://api.example.com/users
JavaScript выполняет:
Cross-origin fetch
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const users = await response.json();Поскольку origins различаются, браузер применяет CORS. В запросе он может отправить:
Origin: https://app.example.comСервер разрешает frontend ответом:
Access-Control-Allow-Origin: https://app.example.comПосле этого браузер позволяет JavaScript получить response.
Подробнее о самом fetch(), Promise и обработке ошибок есть в материале ResizeWeb про асинхронный JavaScript.
Важный нюанс: CORS не обязательно останавливает запрос
Фраза «CORS заблокировал запрос» удобна, но технически часто неточна.
Для некоторых cross-origin запросов браузер действительно отправляет HTTP-запрос серверу. Сервер может его обработать и вернуть ответ. Затем браузер проверяет CORS headers и запрещает JavaScript страницы прочитать ответ.
CORS не является защитой API от запросов. Если операция требует авторизации, защищайте её authentication и authorization на сервере. Клиент без браузерной CORS-проверки — например, другой backend — всё равно способен отправить HTTP-запрос.
Когда preflight не нужен
Не каждый cross-origin запрос вызывает OPTIONS. Fetch Standard определяет набор CORS-safelisted методов и headers, для которых предварительная проверка не требуется.
Упрощённый пример:
fetch('https://api.example.com/public')Такой GET без нестандартных headers обычно не требует preflight.
Даже POST не автоматически означает preflight. Значение имеют метод, headers и Content-Type.
Поэтому правило «GET и POST — простые, PUT и DELETE — сложные» тоже слишком грубое. Например, POST с Content-Type: application/json обычно приводит к preflight.
Что такое CORS preflight
Preflight — предварительный запрос OPTIONS, с помощью которого браузер выясняет, разрешает ли сервер будущий cross-origin запрос.
Например frontend хочет выполнить:
Запрос, вызывающий preflight
await fetch('https://api.example.com/users/42', {
method: 'DELETE',
headers: {
Authorization: 'Bearer TOKEN'
}
});Перед DELETE браузер может отправить:
OPTIONS /users/42
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorizationА сервер должен ответить политикой, допускающей этот запрос:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: DELETE
Access-Control-Allow-Headers: AuthorizationТолько после успешной проверки браузер отправляет actual request.
Как проверить preflight вручную
Браузер формирует CORS preflight автоматически, но похожий запрос удобно воспроизвести через curl при диагностике сервера.
Проверка CORS preflight через curl
curl -i -X OPTIONS 'https://api.example.com/users/42' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: DELETE' \
-H 'Access-Control-Request-Headers: Authorization, Content-Type'Проверьте, что response содержит нужные Access-Control-Allow-* headers и что OPTIONS не получает 301, 401, 403, 404 или 500 раньше CORS middleware.
Основные CORS-заголовки
| Header | Кто отправляет | Назначение |
|---|---|---|
Origin | Браузер | Origin страницы, инициировавшей запрос |
Access-Control-Allow-Origin | Сервер | Какому origin разрешено читать response |
Access-Control-Request-Method | Браузер в preflight | Метод будущего actual request |
Access-Control-Allow-Methods | Сервер | Разрешённые методы |
Access-Control-Request-Headers | Браузер в preflight | Headers будущего запроса |
Access-Control-Allow-Headers | Сервер | Разрешённые request headers |
Access-Control-Allow-Credentials | Сервер | Разрешает credentialed CORS response |
Access-Control-Expose-Headers | Сервер | Открывает JavaScript дополнительные response headers |
Access-Control-Max-Age | Сервер | Позволяет браузеру кэшировать результат preflight |
Access-Control-Allow-Origin: * или конкретный домен?
Для действительно публичного ресурса без credentials возможна политика:
Access-Control-Allow-Origin: *Для приложения с определённым frontend лучше сузить доступ:
Access-Control-Allow-Origin: https://app.example.comЕсли сервер динамически отражает один из разрешённых origins, response обычно должен также учитывать кеширование:
Vary: OriginНе отражайте произвольный Origin в Access-Control-Allow-Origin без allowlist. Конструкция «получил Origin → без проверки вернул его обратно» фактически превращает вашу политику в разрешение для любого origin.
CORS с cookies и credentials
Для cross-origin запроса с cookies frontend может использовать:
Fetch с credentials
const response = await fetch('https://api.example.com/profile', {
credentials: 'include'
});Сервер должен вернуть конкретный origin:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: trueЗдесь нельзя заменить origin на *.
CORS — только одна часть работы с cookies. На фактическую отправку cookie также влияют её атрибуты, включая SameSite, Secure и доменную область.
CORS не заменяет защиту от CSRF
Это важное следствие предыдущего раздела. Сервер может принять запрос даже тогда, когда браузер позже скроет response от JavaScript.
Поэтому API с cookie-аутентификацией нельзя защищать от изменения данных только CORS-политикой. Нужны подходящие серверные меры: корректная авторизация, настройка cookie, а при соответствующей модели угроз — CSRF-защита.
Почему mode: "no-cors" обычно не исправляет CORS
Одна из популярных попыток:
fetch(url, {
mode: 'no-cors'
});Название создаёт впечатление, будто CORS отключится и JavaScript получит данные. Но cross-origin response в этом режиме становится opaque: JavaScript не получает нормальный доступ к body и headers.
Нужно прочитать JSON чужого API? no-cors не решает задачу.
Контролируете API? Настройте правильные response headers.
Не контролируете API? Используйте его официальный механизм доступа или собственный backend, если это допускается правилами API.
Ошибка: No Access-Control-Allow-Origin header
Сообщение:
No 'Access-Control-Allow-Origin' header is present on the requested resourceозначает, что браузер не получил CORS-разрешение для текущего origin.
Проверьте:
приходит ли response вообще;
есть ли
Access-Control-Allow-Origin;совпадает ли значение с
Originзапроса;добавляется ли header также в ответы с ошибками;
не удаляет ли header reverse proxy;
не отвечает ли другой upstream/server block.
Ошибка: CORS preflight did not succeed
Откройте в Network именно OPTIONS. Причина иногда вообще находится ниже уровня CORS: , TLS, redirect, authentication middleware или неправильный route.
OPTIONS должен доходить до нужного server/location.
Preflight не должен требовать обычную пользовательскую авторизацию.
Response должен содержать подходящий
Access-Control-Allow-Origin.Метод actual request должен быть разрешён.
Запрошенные headers должны быть разрешены.
После успешного OPTIONS проверьте CORS headers ещё раз на actual response.
Ошибка: Request header field Authorization is not allowed
Если frontend отправляет:
Authorization: Bearer ...preflight сообщает серверу о будущем header через Access-Control-Request-Headers.
Политика должна допускать его, например:
Access-Control-Allow-Headers: Content-Type, AuthorizationНе добавляйте огромный список headers «на всякий случай». Разрешайте то, что действительно используется клиентом.
Mixed Content — это не ошибка CORS
Если страница загружена по HTTPS и обращается к небезопасному HTTP endpoint, браузер может заблокировать запрос как Mixed Content.
Добавление Access-Control-Allow-Origin эту проблему не исправит.
Production-решение — доступ к frontend и API по HTTPS.
Готовая настройка CORS в Express.js
Express поддерживает популярный middleware cors, который устанавливает CORS response headers.
Express: CORS для одного frontend
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
app.get('/api/profile', (req, res) => {
res.json({ id: 42 });
});
app.listen(3000);При application-level app.use(cors(...)) middleware умеет обрабатывать preflight для маршрутов приложения. Если CORS назначается точечно, убедитесь, что OPTIONS также обслуживается.
Для публичного API без cookies конфигурация может быть другой. Не копируйте credentials: true автоматически: включайте credentials только там, где они действительно нужны.
Готовая настройка CORS в Nginx
Nginx может добавлять CORS headers на уровне reverse proxy. Но именно здесь часто возникают ошибки из-за обработки OPTIONS, кодов ответа и динамических origins.
Для одного известного frontend базовая схема может выглядеть так:
Nginx: один разрешённый frontend
location /api/ {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary "Origin" always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age "600" always;
add_header Content-Length 0;
return 204;
}
proxy_pass http://backend;
}Параметр always полезен, когда CORS header должен присутствовать не только на типичных успешных ответах. Перед копированием конфигурации определите реальные методы, headers и origin вашего приложения.
Не настраивайте CORS одновременно независимо в Nginx и backend, пока не понимаете, какой слой отвечает за политику. Дублирующиеся Access-Control-Allow-Origin headers способны сами вызвать CORS error.
Если Nginx используется не только как proxy, но и для распределения трафика, полезен отдельный материал ResizeWeb о балансировке нагрузки и настройке Nginx.
Готовая настройка CORS в FastAPI
В FastAPI используется CORSMiddleware.
FastAPI: CORS allowlist
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.example.com",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
@app.get("/api/profile")
async def profile():
return {"id": 42}FastAPI по умолчанию использует ограничительные значения CORS-параметров, поэтому разрешения задаются явно. Для credentials также лучше использовать явный список origins вместо wildcard.
Как разрешить несколько frontend-доменов
Если есть production, staging и административная панель, политика может основываться на allowlist.
Логика:
allowedOrigins = [
"https://app.example.com",
"https://admin.example.com",
"https://staging.example.com"
]При запросе сервер сравнивает Origin со списком и только при совпадении возвращает его в Access-Control-Allow-Origin.
Проверка через простое origin.endsWith("example.com") может быть опасной: строка вроде notexample.com тоже заканчивается на example.com. Сравнивайте нормализованные origins или используйте аккуратно проверенное правило для поддоменов.
Почему CORS постоянно возникает на localhost
Типичная разработка:
http://localhost:5173 → frontend
http://localhost:3000 → APIHost одинаковый, но port различается — значит origins разные.
Есть два нормальных подхода:
разрешить development origin на backend;
настроить dev proxy в Vite/Webpack/другом dev server и обращаться к API через тот же origin.
Не переносите без необходимости development-политику вроде широкого * в production.
Как «обойти CORS», если чужой API его не разрешает
Если API принадлежит другой компании и не разрешает ваш frontend origin, JavaScript страницы не может заставить этот API изменить свою CORS-политику.
Возможные варианты:
использовать официальный browser API или разрешённый origin;
попросить владельца API добавить ваш origin;
если условия API разрешают server-to-server доступ — выполнять запрос на своём backend;
использовать официальный SDK или предусмотренную интеграционную схему.
Публичный «CORS proxy» не является нормальной production-архитектурой для токенов и пользовательских данных. Вы передаёте запрос и иногда секреты третьей стороне, которую должны отдельно оценивать с точки зрения безопасности и правил API.
Нужно ли отключать CORS в браузере
Для обычной разработки приложения — нет.
Запуск браузера с отключённой web security может временно помочь доказать, что проблема действительно связана с browser policy, но не исправляет серверную конфигурацию и не воспроизводит условия реальных пользователей.
Исправление должно работать в обычном браузере без специальных флагов.
CORS и безопасность: насколько узкой должна быть политика
MDN рекомендует разрешать минимально необходимое количество origins и ресурсов. Это хорошая production-модель: сначала определить, кому действительно нужно читать API из браузера, и только затем выдавать CORS-разрешения.
| Сценарий | Разумная отправная политика |
|---|---|
| Публичный read-only API без credentials | Возможно *, если данные действительно публичные |
| SPA + private API | Явный frontend origin |
| Несколько собственных frontend | Allowlist |
| Cookies / session | Явный origin + credentials |
| Любой origin с пользовательскими данными | Не делать так автоматически; пересмотреть модель доступа |
Чек-лист грамотной настройки CORS
Запишите точный frontend origin: scheme + host + port.
Определите endpoints, которым действительно нужен cross-origin browser access.
Разрешите только необходимые origins.
Разрешите только используемые HTTP methods.
Разрешите необходимые request headers.
Проверьте OPTIONS отдельно от actual request.
Если нужны cookies — настройте credentials и не используйте wildcard origin.
При динамическом origin учитывайте
Vary: Origin.Убедитесь, что CORS headers есть и на relevant error responses.
Проверьте, не дублирует ли политику Nginx, или API gateway.
Протестируйте из настоящего browser origin, а не только через Postman.
Не используйте CORS как замену authentication, authorization и CSRF-защите.
Алгоритм диагностики CORS за несколько минут
Шаг 1. В Network найдите failing request.
Шаг 2. Скопируйте его Origin.
Шаг 3. Если есть OPTIONS — сначала исследуйте его.
Шаг 4. Сравните запрошенные method/headers с Access-Control-Allow-Methods и Access-Control-Allow-Headers.
Шаг 5. Проверьте Access-Control-Allow-Origin на response.
Шаг 6. Для cookies проверьте credentials и Access-Control-Allow-Credentials.
Шаг 7. Воспроизведите OPTIONS через curl, если подозрение падает на server/proxy.
Шаг 8. Если curl работает, а browser нет — сравните реальные headers запросов, origin и credential mode.
Именно server-side часть обычно находится в зоне ответственности backend-разработчика, а понимание того, как браузер формирует fetch() и обрабатывает response, необходимо и JavaScript-разработчику.
Коротко об истории CORS
Same-Origin Policy возникла в раннюю эпоху JavaScript как способ отделить контекст одного сайта от другого. По мере развития AJAX и web API разработчикам всё чаще требовался контролируемый cross-origin доступ.
Появлялись обходные механизмы вроде JSONP, но веб-платформе требовалась единая модель, в которой сервер мог явно дать браузеру разрешение на чтение cross-origin response.
Так сформировался CORS. Сегодня его алгоритмы определяются Fetch Standard, а браузеры применяют их автоматически при соответствующих запросах через fetch(), XMLHttpRequest и другие web APIs.
Практически полезнее помнить не даты, а архитектурную эволюцию:
Same-Origin Policy → необходимость легального cross-origin доступа → CORS headers и preflight → современная Fetch-модель
Официальная документация
MDN: Cross-Origin Resource Sharing — практическое описание CORS, preflight и headers.
WHATWG Fetch Standard — нормативные алгоритмы современной CORS-модели.
Express: cors middleware — параметры middleware и preflight.
Nginx: ngx_http_headers_module — поведение
add_header.FastAPI: CORS — конфигурация
CORSMiddleware.
Что нужно запомнить о CORS
CORS — это механизм controlled cross-origin access в браузере. Сервер через HTTP headers говорит браузеру, каким origins разрешено читать response.
Большинство проблем сводятся к пяти вещам: неправильный origin, неудачный preflight, запрещённый method, запрещённый header или неверная схема credentials.
Если видите CORS error, не начинайте с mode: "no-cors" и не отключайте безопасность браузера. Откройте Network, найдите Origin и OPTIONS, посмотрите фактические response headers и настройте allowlist на том серверном слое, который действительно отвечает клиенту.
Частые вопросы о CORS
Что такое CORS простыми словами?
CORS — правила, по которым сервер сообщает браузеру, разрешено ли JavaScript с другого origin читать его response.
Что означает Access-Control-Allow-Origin?
Это response header сервера. Он указывает origin, которому браузер может открыть доступ к cross-origin response, либо * для подходящих публичных сценариев без credentials.
Что такое preflight?
Это автоматический предварительный OPTIONS-запрос браузера. Он проверяет, разрешает ли сервер будущие method и headers до отправки actual request.
Почему CORS работает в Postman, но не в браузере?
Потому что CORS является browser-enforced механизмом. Postman, curl и server-to-server клиенты не применяют browser Same-Origin Policy.
Можно ли исправить CORS на frontend?
Обычно нет, если проблема заключается в политике чужого сервера. Разрешение приходит в response от сервера. Frontend может исправить неверный URL, лишний header или credential mode, но не способен сам добавить разрешающий response header удалённому API.
Поможет ли mode: no-cors?
Для чтения JSON API — обычно нет. Cross-origin response становится opaque, поэтому JavaScript не получает обычный доступ к body и headers.
Можно ли использовать Access-Control-Allow-Origin: *?
Да, для действительно публичных ресурсов в подходящих сценариях. Для credentialed response нужно указывать конкретный origin.
CORS защищает API от злоумышленников?
Нет. CORS ограничивает доступ browser JavaScript к response. Authentication и authorization должны выполняться отдельно на сервере.
Почему localhost:3000 и localhost:5173 считаются разными origin?
Origin включает port. Поэтому одинаковый hostname с разными портами образует разные origins.
Нужно ли разрешать OPTIONS в авторизации?
Preflight должен получить CORS-ответ до actual credentialed request. Если authentication middleware возвращает ошибку на OPTIONS раньше CORS-обработчика, браузер не сможет завершить preflight.
Комментарии 0
Обсудите материал, задайте вопрос или дополните его своим опытом.
Комментарии доступны зарегистрированным пользователям. Почту нужно подтвердить один раз.
Начните обсуждение: задайте вопрос автору или поделитесь практическим опытом.