Разбираемся с CORS. Необходимая браузерная защита

Безопасность

Сколько раз вы сталкивались с ошибкой вида Access to ... from origin ... has been blocked by CORS policy.? Уверен, в вашей практике это происходит плюс-минус регулярно: браузер ругается, вы не понимаете, в чем дело, при этом бэкендеры говорят: «Я тестировал через постман, ручка отрабатывает, смотри у себя». В этот момент мы лезем крутить настройки прокси или пытаемся понять, что мы сделали не так, где оступились

Давайте разберемся с CORS раз и навсегда. Это суперсила браузера, которой нужно научиться пользоваться.

Базовые термины: CORS, SOP

CORS — Cross Origin Resource Sharing — браузерный механизм безопасности, который призван навести порядок при взаимодействии двух разных ресурсов. Грубо говоря, если Сайт А размещен на источнике А и делает запрос к API, размещенном там же, корсы тут отрабатывать не будут.

Можно представить, что мы берем что-то у себя из левого кармана и перекладываем в правый. Никто не может запретить нам сделать это. Но ситуация выглядит чуть более тревожно, если кто-то кладет что-то в наш карман. Даже если по нашей просьбе.

Вот и браузер включает оборону, если видит что запрос с источника А улетает на источник Б.

Здесь важно понять, как браузер различает источники. Тут и подходит время рассказать про второй базовый термит — Same Origin Policy или политику одного источника.

Origin, источник — своего рода идентификатор. Он складывается из трех частей:

  1. Протокол (http:// или https://)
  2. Домен (мой-сайт.ру или localhost)
  3. Порт (:3000:8080)

Если отличается хотя бы одна из этих трех частей, браузер считает источники разными

  • http://site.ru и https://site.ru — разные (другой протокол).
  • http://site.ru и http://api.site.ru — разные (поддомен).
  • http://localhost:3000 и http://localhost:4000 — разные (порт).

Именно поэтому запрос с localhost:3000 на localhost:4000 или на api.mycorp.ru может быть заблокирован.

Зачем все это нужно? Защищаемся от CSRF

Представим, что мы вернулись в прошлое и у нас уже есть интернет, но CORS еще не существует (кстати, они появились аж в 2004 году). Мы авторизованы в банке bank.ru. Во второй вкладке у нас открыта почта, куда упало письмо со зловредной ссылкой на сайт evilhacker.com. Мы переходим по ссылке, сайт загружается и на нем исполняется какой-нибудь такой скрипт:

fetch('https://bank.ru/transfer?to=hacker&amount=1000', {
   credentials: 'include' // браузер прикрепит ваши cookies!
})

Да, такое апи банка выглядит дырявым и уязвимым (думаю автора метода, который переводить клиентские деньги по гет-запросу могли бы с позором уволить), но дело в другом. Напомню, что мы находимся в мире без CORS. Браузер послушно сходит на bank.ru, прикрепит ваши куки, и тот отправит деньги злоумышленнику.

Вернемся в сегодняшний день. CORS изобрели, bank.ru переписал метод на POST с отправкой параметров в теле запроса. Мы снова попадаемся на удочку злоумышленника и открываем его сайт, исполняем его скрипт. Но вот что происходит:

  • браузер фоново отправляет preflight-запрос методом OPTIONS на адрес https://bank.ru/transer
  • браузер смотрит на заголовок Access-Control-Allow-Origin в ответе и ищет, есть ли в перечне допущенных к этому методу ресурсов http://evil.com
  • надеемся, что не находит и не отправляет запрос, блокируя его механизмом CORS

Это сработает для всех сложных запросов. К таковым относятся например те, которые отправляются с заголовком Content-Type отличным от application/x-www-form-urlencoded, multipart/form-data или text/plain, используют методы PUT / DELETE или собственные заголовки.

Разберем механизм CORS по шагам

Допустим, ваш фронт на front.ru:3000 стучится на back.ru.

1. Отправляем запрос:

fetch('https://back.ru/api/users')
  1. Браузер видит: Origin front.ru:3000 не совпадает с back.ru. Браузер добавляет в запрос заголовок:
Origin: http://front.ru:3000

3. Сервер back.ru получает запрос, смотрит на Origin и решает, пропускать ли его или нет. Если пускает, сервер добавляет в ответ заголовок:

Access-Control-Allow-Origin: http://front.ru:3000

4. Браузер получает ответ. Если заголовка Access-Control-Allow-Origin нет — браузер выбрасывает ошибку CORS

CORS на этапе разработки: прокси наше всё

Покажу на примере Vite. Просто добавим в vite.config.js:

proxy: {
  '/api': 'http://localhost:5000'
}

Теперь с фронтенда мы будем стучаться на свой же http://localhost:3000/api, а Vite уже сам проксирует на бэкенд, расположенный на 5000 порту. CORS не возникает, потому что для браузера Origin совпадает. Мы перекладываем JSON из кармана в карман.

Итог: что нужно запомнить

  • CORS — браузерная, а не серверная защита
  • Если возникает ошибка CORS, начинать копать нужно с настроек бэкенда
  • Для разработки проще всего настроить прокси на дев-сервере, чтобы фронтенд и бэкенд имели одинаковый Origin

Мы не в Хогвартсе, поэтому никакой магии тут нет. Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу