CORS: зачем и как работает

CORS: зачем и как работает

По умолчанию JavaScript в браузере может отправлять HTTP-запросы только к тому же домену, с которого загружена страница. Это ограничение безопасности называется Same-Origin Policy (SOP). Но современный веб построен на кросс-доменных запросах: фронтенд на example.com, API на api.example.com, картинки на CDN. Механизм, который разрешает эти запросы безопасно — CORS (Cross-Origin Resource Sharing).

Same-Origin Policy: почему нельзя просто так

Браузер считает два URL одним origin, если совпадают протокол, домен и порт:

https://example.com:443/page   ← origin: https://example.com:443
https://example.com/page2      ← ТОТ ЖЕ origin (порт по умолчанию 443)
http://example.com              ← РАЗНЫЙ (протокол http ≠ https)
https://api.example.com         ← РАЗНЫЙ (поддомен api ≠ www)
https://example.com:8443        ← РАЗНЫЙ (порт 8443 ≠ 443)

Без SOP любой сайт мог бы через JavaScript отправить запрос к твоему банку, и браузер автоматически прикрепил бы cookies сессии. Same-Origin Policy защищает именно от этого: сайт злоумышленника не может прочитать ответ с другого origin'а.

Важно: SOP блокирует чтение ответа, а не сам запрос. Сервер получает запрос и выполняет его, но браузер не отдаёт ответ JavaScript'у. Это защищает данные, но не защищает от атак вроде CSRF (разберём в уроке 11-3).

Когда нужен CORS

CORS нужен, когда законный сайт хочет сделать кросс-доменный запрос:

  • Фронтенд на app.example.com обращается к API api.example.com.
  • SPA на localhost:5173 (dev-сервер Vite) обращается к API localhost:3001.
  • Сторонний виджет (погода, карта) встраивается с другого домена.

Как работает CORS: preflight и простые запросы

CORS работает через HTTP-заголовки. Простые запросы (GET, POST с определёнными Content-Type) отправляются сразу, а браузер проверяет ответ:

Клиент (app.example.com):
  GET /api/data HTTP/1.1
  Origin: https://app.example.com

Сервер (api.example.com):
  HTTP/1.1 200 OK
  Access-Control-Allow-Origin: https://app.example.com

Браузер: Origin в ответе совпадает с Origin запроса — ОК, JavaScript получает данные

Если сервер не отправил Access-Control-Allow-Origin или отправил другой origin, браузер блокирует ответ и выбрасывает ошибку в консоль:

Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy

Preflight-запросы для не-простых запросов (PUT, DELETE, кастомные заголовки, Content-Type: application/json). Браузер сначала отправляет OPTIONS-запрос:

OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization

Сервер:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization

Только после успешного OPTIONS браузер отправляет сам DELETE-запрос. Preflight кэшируется (Access-Control-Max-Age: 3600), чтобы не слать OPTIONS перед каждым запросом.

CORS и credentials

По умолчанию кросс-доменные запросы НЕ отправляют cookies и HTTP-аутентификацию. Чтобы включить их, нужны два флага:

Клиент (fetch):  credentials: 'include'

Сервер:
  Access-Control-Allow-Origin: https://app.example.com   (НЕ *, конкретный origin обязателен)
  Access-Control-Allow-Credentials: true

Без Access-Control-Allow-Credentials: true браузер проигнорирует Set-Cookie в кросс-доменном ответе.

Настройка CORS на сервере

CORS настраивается на сервере. В Express это делается пакетом cors:

const cors = require('cors');

app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 3600
}));

Для dev-режима часто разрешают всё: origin: '*'. В production звёздочка несовместима с credentials: true, и нужно перечислить конкретные домены.

Проверь себя

  1. Что такое Same-Origin Policy и зачем она нужна?
  2. Зачем нужен preflight-запрос (OPTIONS)?
  3. Почему Access-Control-Allow-Origin: * не работает вместе с credentials: 'include'?
<details> <summary>Ответы</summary>
  1. SOP — ограничение браузера, запрещающее JavaScript читать ответы с другого origin (протокол + домен + порт). Защищает от кражи данных через кросс-доменные запросы.
  2. Preflight — запрос OPTIONS перед не-простым запросом. Сервер явно разрешает метод и заголовки. Защищает от атак, которые могли бы выполниться простым GET/POST.
  3. * означает «любой origin», а credentials: 'include' отправляет cookies конкретному сайту. Комбинация опасна — злоумышленник мог бы получить данные с куками жертвы. Браузер требует конкретный origin.
</details>

Что унести с урока

  • Same-Origin Policy блокирует чтение кросс-доменных ответов в JavaScript.
  • CORS — механизм для разрешения легитимных кросс-доменных запросов через заголовки.
  • Простые запросы отправляются сразу (браузер проверяет ответ). Не-простые требуют preflight (OPTIONS).
  • credentials: 'include' требует конкретный origin и Allow-Credentials: true.

В следующем уроке разберём Content Security Policy (CSP) — как сказать браузеру, откуда можно загружать ресурсы и скрипты.

Попробуйте интерактивную версию

Практические задачи, квизы и AI-наставник — бесплатный старт без карты

Перейти к практике