CORS
| Полное название | Cross-Origin Resource Sharing |
|---|---|
| Стандарт | WHATWG Fetch Standard (ранее рекомендация W3C, 2014) |
| Основа | политика одного источника (same-origin policy) |
| Источник (origin) | схема + хост + порт |
| Ключевой заголовок | Access-Control-Allow-Origin |
| Предварительный запрос | метод OPTIONS (preflight) |
| Где действует | только в браузерах |
CORS (англ. Cross-Origin Resource Sharing — совместное использование ресурсов между источниками) — механизм, с помощью которого сервер сообщает браузеру, каким сайтам с других адресов разрешено читать его ответы. По умолчанию браузер запрещает скрипту на странице https://app.example.com читать ответ от https://api.other.com — это политика одного источника (same-origin policy). CORS — способ контролируемо ослабить этот запрет с помощью специальных HTTP-заголовков.[1][2] Ошибка «blocked by CORS policy» в консоли браузера — одна из самых частых у веб-разработчиков.
Политика одного источника
Источник (origin) — сочетание схемы, хоста и порта.[3]
| Адрес | Тот же источник, что https://example.com? |
|---|---|
https://example.com/page |
да |
http://example.com |
нет — другая схема |
https://api.example.com |
нет — другой хост |
https://example.com:8443 |
нет — другой порт |
Политика защищает пользователя: без неё любой открытый сайт мог бы скриптом отправить запрос в ваш интернет-банк — браузер приложил бы ваши cookie — и прочитать ответ. Важно, что ограничение касается чтения ответа скриптом; загрузку картинок, скриптов, отправку форм с других источников политика не запрещает (от подделки таких запросов защищаются иначе, см. CSRF).
Как работает CORS
Браузер добавляет к запросу между источниками заголовок Origin, а сервер в ответе указывает, разрешён ли этот источник:
GET /v1/items HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Content-Type: application/json
Если заголовка нет или значение не совпадает, браузер всё равно получает ответ, но не отдаёт его скрипту и пишет ошибку в консоль. Сервер при этом запрос уже выполнил — CORS не защищает сервер, а защищает данные пользователя в браузере.
Простые и предварительные запросы
Простые запросы — GET, HEAD или POST с ограниченным набором заголовков и типами тела text/plain, multipart/form-data, application/x-www-form-urlencoded — отправляются сразу. Они не дают ничего, чего нельзя было сделать обычной HTML-формой.
Всё остальное — PUT, DELETE, PATCH, тело application/json, заголовок Authorization — требует предварительного запроса (preflight): браузер сначала отправляет OPTIONS и спрашивает разрешения.[1]
OPTIONS /v1/items/42 HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 600
Access-Control-Max-Age разрешает браузеру кешировать ответ, чтобы не повторять предварительный запрос перед каждым вызовом.
Заголовки
| Заголовок ответа | Назначение |
|---|---|
Access-Control-Allow-Origin |
разрешённый источник или * |
Access-Control-Allow-Methods |
разрешённые методы (для preflight) |
Access-Control-Allow-Headers |
разрешённые заголовки запроса |
Access-Control-Allow-Credentials: true |
можно отправлять cookie и HTTP-аутентификацию |
Access-Control-Expose-Headers |
какие заголовки ответа доступны скрипту |
Access-Control-Max-Age |
время кеширования результата preflight, в секундах |
Учётные данные. По умолчанию fetch не отправляет cookie на другие источники. Если клиент указал credentials: "include", сервер обязан ответить Access-Control-Allow-Credentials: true и конкретным источником — подстановочный * в этом случае запрещён.
Пример на сервере
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["GET", "POST", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
allow_credentials=True,
)
Готовые решения есть во всех фреймворках (см. FastAPI); заголовки можно добавлять и на уровне обратного прокси.
Частые ошибки
- Отражение любого
OriginвAccess-Control-Allow-Originвместе сAllow-Credentials: true— фактически отключает защиту: любой сайт сможет читать данные пользователя.[6] Проверяйте источник по точному списку. - Проверка по подстроке:
example.com.evil.netпроходит проверку «содержит example.com». - Разрешение источника
null— его можно получить из песочницыiframeили локального файла. - Забытый
Vary: Originпри динамическом ответе — CDN закеширует ответ для одного источника и отдаст другому. - Попытка «починить» CORS на клиенте — заголовки разрешений выставляет только сервер. Обход через
mode: "no-cors"даёт непрозрачный ответ, который нельзя прочитать.
Частое и самое простое решение — избежать запросов между источниками вовсе: отдавать фронтенд и API с одного домена, проксируя /api на бэкенд.[5]
CORS — механизм браузеров. Серверные программы, curl и мобильные приложения его не применяют, поэтому он не заменяет аутентификацию API.