Same-origin policy
Браузер изолирует сайты друг от друга по origin: это тройка схема + хост + порт. https://app.ru и https://api.app.ru разные origin (хост другой). http://app.ru и https://app.ru тоже разные (схема). https://app.ru:443 и https://app.ru одинаковые: 443 порт по умолчанию для https.
Same-origin policy (SOP) запрещает скрипту одного origin читать ответы другого origin. Важно: запрет касается чтения ответа в JS. Отправить запрос браузер часто может (форма, <img>, fetch в режиме no-cors), но прочитать тело не даст.
Что такое CORS
CORS (Cross-Origin Resource Sharing) это способ для сервера разрешить чужим origin читать его ответы. Сервер говорит заголовками ответа, кому можно:
Access-Control-Allow-Origin: https://app.ru
Проверку делает браузер. Сервер ответ отдаёт всегда, просто браузер не покажет его скрипту, если заголовков нет. Поэтому CORS не защищает сервер: curl, Postman и бэкенд-сервисы его не соблюдают. CORS защищает пользователя от чужих сайтов, которые хотели бы прочитать данные от его имени.
Простые запросы и preflight
Простой запрос уходит сразу. Условия: метод GET, HEAD или POST; только «безопасные» заголовки (Accept, Accept-Language, Content-Language, Content-Type); Content-Type один из text/plain, multipart/form-data, application/x-www-form-urlencoded. Браузер добавляет Origin, а потом смотрит на Access-Control-Allow-Origin в ответе.
Всё остальное требует preflight: браузер сначала шлёт OPTIONS:
OPTIONS /orders HTTP/1.1
Origin: https://app.ru
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
Сервер отвечает, что разрешает:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.ru
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 600
Только после этого уходит настоящий PUT. POST с Content-Type: application/json уже не простой запрос, на него будет preflight. Access-Control-Max-Age позволяет кешировать результат preflight (браузеры режут максимум: Chrome до 2 часов).
Куки и credentials
По умолчанию fetch на чужой origin куки не шлёт. Чтобы послать: fetch(url, { credentials: 'include' }) (в axios withCredentials: true). Тогда сервер обязан ответить:
Access-Control-Allow-Origin: https://app.ru
Access-Control-Allow-Credentials: true
И здесь * запрещена: с credentials нужен конкретный origin. Кроме того, сама кука должна дойти: для кросс-сайтового запроса ей нужен SameSite=None; Secure.
Чтение заголовков ответа
Скрипт видит только безопасные заголовки ответа (Content-Type, Cache-Control и ещё несколько). Свои, например X-Total-Count, надо открыть:
Access-Control-Expose-Headers: X-Total-Count
Типичная настройка на сервере
const allowed = new Set(['https://app.ru', 'https://admin.app.ru']);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowed.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin');
}
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
res.setHeader('Access-Control-Allow-Headers', 'content-type,authorization');
return res.sendStatus(204);
}
next();
});
Vary: Origin нужен, чтобы CDN не отдал ответ с чужим Allow-Origin другому сайту.
Частые ошибки
- Думать, что CORS настраивается на фронте. Нет: заголовки отдаёт сервер. С фронта можно только обойти через свой прокси (dev-сервер Vite, nginx).
- Отражать любой
Originв ответ вместе сAllow-Credentials: true. Это равносильно отключению защиты: любой сайт прочитает данные пользователя. - Забыть обработать
OPTIONS: роутер отвечает 404 или требует авторизацию, preflight падает. - Путать CORS и CSRF. CORS про чтение ответа, CSRF про отправку запроса от имени пользователя. Простой POST с формы уйдёт и без CORS.
Что спросят на собесе
- Что такое origin и same-origin policy?
- Когда браузер делает preflight и что в нём передаётся?
- Почему нельзя
Access-Control-Allow-Origin: *с куками? - Кто проверяет CORS и защищает ли он сервер?
- Как обойти CORS в разработке?