Урок по треку Сети и HTTP 6 мин

CORS без магии

Same-origin policy, простые и preflight-запросы, куки между доменами и типичные ошибки настройки.

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 в разработке?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.