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обращается к APIapi.example.com. - SPA на
localhost:5173(dev-сервер Vite) обращается к APIlocalhost: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, и нужно перечислить конкретные домены.
Проверь себя
- Что такое Same-Origin Policy и зачем она нужна?
- Зачем нужен preflight-запрос (OPTIONS)?
- Почему
Access-Control-Allow-Origin: *не работает вместе сcredentials: 'include'?
- SOP — ограничение браузера, запрещающее JavaScript читать ответы с другого origin (протокол + домен + порт). Защищает от кражи данных через кросс-доменные запросы.
- Preflight — запрос OPTIONS перед не-простым запросом. Сервер явно разрешает метод и заголовки. Защищает от атак, которые могли бы выполниться простым GET/POST.
*означает «любой origin», аcredentials: 'include'отправляет cookies конкретному сайту. Комбинация опасна — злоумышленник мог бы получить данные с куками жертвы. Браузер требует конкретный origin.
Что унести с урока
- Same-Origin Policy блокирует чтение кросс-доменных ответов в JavaScript.
- CORS — механизм для разрешения легитимных кросс-доменных запросов через заголовки.
- Простые запросы отправляются сразу (браузер проверяет ответ). Не-простые требуют preflight (OPTIONS).
credentials: 'include'требует конкретный origin иAllow-Credentials: true.
В следующем уроке разберём Content Security Policy (CSP) — как сказать браузеру, откуда можно загружать ресурсы и скрипты.