Вы когда-нибудь видели в консоли сообщение вроде: «Access to fetch at '…' from origin '…' has been blocked by CORS policy»? Это как в том фильме: «Суслика видишь? — А он есть». CORS не бросается в глаза, пока все работает, но в нужный момент жестко пресекает недопустимые действия. Например, чтение ответа на кросс-запрос без разрешения сервера.

Веб-технологии позволяют нам мгновенно совершать банковские операции, платить в магазинах, собирать и обрабатывать данные — но чем активнее сайты общаются между собой, тем острее встает вопрос безопасности.

Я знал, что CORS — это защита для кросс-доменных запросов, но в первый раз, столкнувшись с ошибкой, не понял, как её чинить. Подставил в запрос нужный  заголовок, который был в ошибке (далее разберем где можно увидеть эти ошибки) — не помогло. Пришлось нырнуть глубже: понять, что такое origin, чем simple отличается от preflight, почему с credentials звёздочка не прокатывает и где заканчивается CORS, а начинается CSRF (пока сильно не вникайте в сказанное, это раскроется походу статьи). 

Меня зовут Баир, я разработчик в команде fuse8. В этой статье я коротко отвечу на вопросы о том, зачем была создана CORS политика, как она устроена под капотом, почему простого действия типа «поставить заголовок на бэке» может быть мало, и какие безопасные паттерны стоит выбирать во фронтенде. 

Чтобы понять логику CORS, нужно выяснить, с чего вообще эта политика безопасности началась — а именно с политики одного источника (SOP): что она разрешает, что запрещает и почему без неё CORS вообще не нужен.

Что такое SOP и Origin

Началось все в далеком 1995 году, когда появился всеми любимый (или не всеми) JS, и его использование внедрили на веб-страницах. В тот момент и появилась концепция политики браузера и называлась она Same-Origin Policy (SOP) или «Политика одного источника».

SOP – это основополагающий принцип браузерной безопасности, гарантирующий, что скрипты с одного Origin не могут получить доступ к данным другого источника без явного разрешения. Первоначально SOP защищал только доступ к DOM (структуре страницы) других источников, но затем его расширили на другие чувствительные объекты (вроде кук и глобальных JS-объектов). 

А что же такое Origin)? Origin (далее по тексту Источник) – это уникальная комбинация схемы (протокола), домена и порта (рис.1). И, если хотя бы один из этих компонентов разнится, один Источник будет отличаться от другого. 

Рис.1
Рис.1

Примеры соответствия или несоответствия источников

В сравнительной таблице ниже приведены примеры:

Исходный URL

Сравниваемый URL

Проверка

Причина

https://site.com

https://site.com/api

Соответствует

https://site.com:81

Не соответствует

Другой порт

http://site.com

Не соответствует

Отличается протокол

https://ru.site.com

Не соответствует

Отличается домен

https://www.site.com

Не соответствует

Отличается домен (требуется полное соответствие)


Как работает SOP на практике

Рис.2
Рис.2

Давайте теперь разберем по шагам сценарий, когда может включаться SOP политика (рис.2):

  1. Пользователь зашел и запросил ресурсы, расположенные по адресу https://www.a.com;

  2. Далее подгруженные ресурсы с адреса https://www.a.com в браузере пользователя инициализируют запрос за ресурсами, расположенные по адресу https://www.b.com;

  3. Браузер проверяет так называемый Источник и, как видно в данном случае, источники не совпадают, поэтому браузер блокирует доступ к ресурсам https://www.b.com

Что было бы без SOP

А почему вообще возникла необходимость в SOP? Какая мне как пользователю от этого польза? А если я веб разработчик, зачем должен держать это в голове и подстраиваться под ограничения?

И тут будет тяжело поспорить с таким аргументом как безопасность.

Рис.3
Рис.3

Давайте представим, что SOP нет, и разыграем сценарий (рис.3), плачевный как для пользователя (который стал уязвим), так и для разработчика (к продукту которого подорвалось доверие):

  1. Пользователь вошёл в свой банк bank.com (в браузере сохранён cookie с сессией). 

  2. Затем он посещает вредоносный сайт bad-site.com, на котором скрыт вредоносный скрипт. 

  3. Этот скрипт иницализирует запрос с сесионными куками на bank.com от лица пользователя. 

  4. И в итоге вредоносный скрипт от вашего лица получает ответ из банка, без вашего ведома!

Без SOP этот скрипт может получить список последних транзакций пользователя, создать новую транзакцию и т. д. Это связано с тем, что, в соответствии с изначальной концепцией Всемирной паутины, браузеры обязаны добавлять данные аутентификации вроде сеансовых cookie-файлов и типов заголовков запроса авторизации на уровне платформы, к сайту банка, основываясь на домене этого сайта. 

Давайте вернемся в нашу реальность, где существует механизм защиты от такой неприятности (рис. 4). Шаги 1-3 из плачевного сценария будут такие же, но вот на четвертом шаге SOP заблокирует доступ к запрошенным ресурсам.

Рис.4
Рис.4

Именно чтобы предотвратить такие атаки, был введен принцип одного источника: браузеры начали автоматически блокировать доступ скриптов одного Источника к данным с другого.

Важно понимать

Хотя JavaScript действительно не имеет прямого доступа к cookie-файлам сеанса банковской сессии, он всё равно может отправлять запросы на банковский сайт с использованием cookie-файлов сеанса банковской сессии, как в ситуациях на рис.3 или рис.4.

Опытные читатели скажут, что можно банально поставить у куки HttpOnly. Однако этот флаг стал стандартом только в 2002 году. А кто-то скажет, что есть же еще SameSite. Но и он появился лишь в 2016 году, а стал стандартом только в 2019-2020 годах. 

SOP ограничивает чтение данных из чужого источника, но не блокирует саму отправку запросов на чужие домены. Браузер по-прежнему автоматически подставлял cookie с bank.com при отправке формы на bank.com – просто скрипт с bad-site.com не узнает, что вернул банк. SOP лишь не даст атакующему прочитать ответ и убедиться, что атака сработала. Для защиты от таких сценариев на серверной стороне нужны дополнительные меры (например, CSRF-токены в формах, установить нужные значения у SameSite и HttpOnly и т.д.). 

Вот это и есть политика одного источника (SOP) в браузере, которая призвана защищать пользователя. В целом выглядит не так уж и сложно, согласитесь. Пользователю не нужно об этом думать, ведь за него уже подумали разработчики браузера и предусмотрели механизм защиты.  Разработчику также нет необходимости как-то специально в своем коде об этом беспокоиться – достаточно следовать установленным правилам. 

Ограничения, которые устанавливает SOP 

Итак, SOP накладывает ряд строгих ограничений на взаимодействие между ресурсами разных Источников:

  • Блокировка доступа к контенту страниц. Скрипт не может читать или изменять содержимое страницы другого домена. Например, JavaScript со страницы site-a.com не получит доступ к DOM, кукам, localStorage или другим данным страницы на site-b.com

  • Изоляция фреймов. Если в страницу внедрен <iframe src="..."> с чужого домена, родительский скрипт не может обратиться к iframe.contentWindow.document этого фрейма (и наоборот) – доступ будет запрещён, пока их origin различаются.

  • Блокировка  доступа к ответам HTTP-запросов через XHR/Fetch. Браузер блокирует получение ответа на AJAX-запросы, отправленные скриптом на другой домен. То есть вы можете отправить fetch на сторонний API, но если их Origin разный, браузер не предоставит скрипту ответ.

  • Исключения (разрешённые загрузки). SOP ограничивает именно доступ к данным со сторонних origin, а не саму загрузку ресурсов. Браузер может без ошибок подгрузить изображения, стили, скрипты, медиа и фреймы с другого домена и использовать их как есть: показать картинку, применить стиль, выполнить скрипт, воспроизвести видео, отрендерить iframe. При этом JavaScript страницы не имеет права читать внутреннее содержимое этих ресурсов (пиксели изображения, правила стилей, DOM фрейма, байты видео и т. п.) – если только сервер источника явно не разрешил доступ через CORS.

По сути, SOP говорит: «чужие данные читать нельзя», и этим обеспечивает базовую изоляцию. Но реальный веб давно стал многодоменным: мы тянем шрифты с CDN, ходим к внешним API, организовываем общение между микросервисами. Как сделать такие обмены законными и безопасными, не ломая изоляцию? Здесь на сцену выходит CORS — набор правил согласованного доступа между различными origin.

Появление и роль CORS

Рис.5
Рис.5

SOP остается фундаментом безопасности браузеров. Но чтобы управляемо пересекать границы между источниками, был стандартизован Cross-Origin Resource Sharing (CORS): он добавляет к SOP явные правила и заголовки, позволяя браузеру, основываясь на ответы серверов, точечно разрешать доступ клиентам из других Источников.

По сути, CORS – это технология браузеров, открывающая доступ веб-страниц к ресурсам другого домена при выполнении определённых условий.

Как работает политика CORS 

Допустим, у нас есть фронтенд-приложение на одном домене (https://www.a.com), которое хочет запросить данные с API на другом домене (https://www.b.com). По умолчанию SOP запрещает скрипту читать ответ. Однако стандарт CORS определяет ряд HTTP-заголовков, с помощью которых сервер (домен B) может сообщить браузеру: «Доверяю домену A, можно дать ему прочитать ответ». И это происходит за счет внедренных заголовков, которыми браузер руководствуется и регулирует доступы между Источниками. 

А теперь подробнее разберем работу CORS и рассмотрим, на какие заголовки браузер опирается. Браузер, выполняя AJAX-запрос (Fetch или XHR) к стороннему ресурсу, автоматически добавляет в запрос заголовок Origin, указывающий текущий origin страницы. Например запрос от страницы http://www.a.com/page.html к ресурсу http://www.b.com/data.json будет выглядить таким образом (рис. 6, шаг 2):

GET /data.json HTTP/1.1 Host: www.b.com Origin: http://www.a.com

Сервер www.b.com, получив такой запрос, может решить разрешить доступ. Для этого он должен включить в ответ заголовок Access-Control-Allow-Origin со значением либо конкретного домена-источника запроса, либо * (звёздочка означает «разрешить для любых источников»). Например: 

Access-Control-Allow-Origin: http://www.a.com

Если браузер видит в ответе Access-Control-Allow-Origin со нужным origin (или *), он не будет блокировать скрипту доступ к полученным данным. Иначе – при отсутствии такого заголовка – произойдёт блокировка: JS-код получит сетевую ошибку вместо данных. 

Рис.6
Рис.6

Кроме основного разрешающего заголовка, стандарт CORS определяет и другие заголовки управления доступом:

  • Access-Control-Allow-Credentials – отвечает за доступ к ресурсам с учетом авторизации. Если этот заголовок установлен в true, браузер даст доступ к такому ответу. Важно: при использовании Allow-Credentials: true значение Allow-Origin не может быть равно «*» – нужно явно указать конкретный домен, иначе браузер проигнорирует ответ.

  • Access-Control-Allow-Methods – перечень HTTP-методов, которые разрешены при обращении к ресурсу. Если скрипт планирует отправлять не только GET, но, скажем, PUT или DELETE, сервер должен указать их в этом заголовке, иначе браузер отклонит доступ.

  • Access-Control-Allow-Headers – аналогично, перечень нестандартных заголовков, которые разрешено использовать в запросе. Например, если фронтенд хочет отправить заголовок X-Custom-Header или Authorization, сервер обязан явно разрешить их через этот заголовок.

  • Access-Control-Max-Age – время (в секундах), на которое результаты проверки можно кешировать. Этот заголовок позволяет браузеру не делать лишних предварительных проверок (о них далее) для повторных запросов в течение указанного времени.

  • Access-Control-Request-Method - заголовок отправляется в preflight запросе (о нем далее) и сообщает серверу планируемый метод основного запроса. 

  • Access-Control-Request-Headers - заголовок отправляется в preflight запросе (о нем далее) и сообщает серверу перечень нестандартных заголовков, которые клиент хочет отправить в основном запросе.

Но хочу отметить, что запросы могут быть разными, если смотреть через призму CORS. Здесь мы подойдем к таким понятиям как «простые» запросы (рис.6) и сложные (рис.7) (требующие предварительной проверки через preflight запрос). 

Простые и сложные запросы в CORS

Простой CORS-запрос – это запрос, который не требует дополнительного «рукопожатия» с сервером. Браузер сразу отправляет его, лишь добавляя заголовок Origin, и ожидает прямого ответа с Access-Control-Allow-Origin. 

А как определяется, что запрос сложный? На какие правила браузер опирается чтобы определять каким является запрос? В этом случае в стандарте есть признаки простого и сложного запроса. 

Давайте их рассмотрим:

Признаки простого запроса

Признаки сложного запроса

Метод запроса – только GET, POST или HEAD

Нарушение одного из признаков простого запроса

Заголовки запроса – только стандартные (также их называют безопасными). Разрешены заголовки, которые браузер ставит сам или которые явно разрешены спецификацией без проверки: например, Accept, Accept-Language, Content-Language, а из пользовательских – только Content-Type некоторых видов.

Content-Type (если указан вручную) – должен быть одним из трёх типов: application/x-www-form-urlencoded, multipart/form-data или text/plain. 

Отсутствие нестандартных заголовков. Нельзя отправлять свои заголовки кроме упомянутых выше. Например, Authorization или X-Custom-Header сразу делают запрос сложным.

Дополнительные условия. Запрос не должен использовать специфичные возможности: не должно быть зарегистрированных обработчиков событий загрузки (xhr.upload.onprogress и подобные) и не должна использоваться потоковая передача тела запроса (Fetch API с ReadableStream).

Если все требования простого запроса выполнены, то он считается простым и браузер отправит его напрямую. Однако стоит нарушить хоть одно условие – например, указать заголовок Authorization для токена, или использовать метод PUT, – как браузер перед основным запросом выполнит специальный предварительный запрос (preflight) с методом OPTIONS на тот же URL. Этот OPTIONS-запрос не содержит тело, но включает заголовки Access-Control-Request-Method (с методом основного запроса) и Access-Control-Request-Headers (список нестандартных заголовков, если есть). 

Таким образом, браузер спрашивает сервер, разрешает ли он запрос с такими параметрами. Сервер должен ответить на preflight-запрос статусом 200 (или 204) без тела, но с указанными ранее заголовками Access-Control-Allow-Methods (перечислить разрешённые методы, напр. PUT), Access-Control-Allow-Headers (перечислить разрешаемые нестандартные заголовки, напр. Authorization, X-Custom-Header) и обязательным Access-Control-Allow-Origin (указать origin или *).

Если браузер получает благоприятный ответ, он продолжит и выполнит реальный запрос (например, PUT с указанными заголовками). И уже на реальный ответ сервер опять приложит Access-Control-Allow-Origin (и, если нужно, Access-Control-Allow-Credentials), чтобы браузер отдал данные скрипту. 

Важно отметить

Весь этот обмен происходит автоматически, без вмешательства фронтенд-разработчика – но если на каком-то шаге сервер не вернёт нужные заголовки, браузер отклонит запрос.

Рис.7
Рис.7

Ошибки CORS 

Разработчики могут увидеть ошибки CORS только через консоль браузера – JavaScript-код в случае нарушения политик получает лишь обобщенную ошибку сети. В консоли же будет указано, какой заголовок отсутствует или что именно заблокировано политикой (Origin, метод, заголовок и т.д.). Для устранения проблемы нужно корректно настроить заголовки на сервере.

Например, могут быть такого рода ошибки:

  • При запросе с origin http://localhost:3000 на другой origin http://localhost:4000, если метод PUT не разрешен, то будет такого вида ошибка в консоли:

  • Если при запросе с origin http://localhost:3000 на другой origin http://localhost:4000 значение заголовка Access-Control-Allow-Credentials не установлено как true:

  • Если при запросе с origin http://localhost:3000 на другой origin http://localhost:4000 заголовок запроса custom-header не разрешен:

Окей, как браузер решает «пускать или нет», мы увидели. А какие ещё способы кросс-доменного взаимодействия существуют и почему их не стоит путать с CORS? Об этом далее.

Альтернативы и смежные механизмы

Помимо рассмотренных SOP и CORS также можно упомянуть следующие механизмы:

  • Обход SOP через document.domain. Исторически для субдоменов придумали обход: страницы aaa.example.com и bbb.example.com могут обе выполнить скрипт, который присваивает document.domain = "example.com", и тогда браузер считает их одним origin. Однако на данный момент этот подход уже устарел и объявлен небезопасным. Например, Chrome планирует полностью отключить возможность устанавливать document.domain, так как это подрывает защиту SOP (ссылка на MDN)

  • Взаимодействие через window.postMessage(). Этот API позволяет безопасно общаться скриптам из разных источников. Например, страница с domain-a.com может отправить сообщение во внедрённый iframe с domain-b.com вызовом iframe.contentWindow.postMessage(data, targetOrigin). Если targetOrigin совпадает (или указано "*" для любых), то на стороне domain-b.com iframe поймает событие message и сможет прочитать данные. Важное свойство – при этом ни родитель, ни iframe не получают доступ к чужому DOM или JS-объектам, они обмениваются только строковыми сообщениями. postMessage – основной способ интеграции между разными приложениями в рамках одного окна/вкладки (например, между виджетом оплаты и сайтом).

  • JSONP (JSON with Padding). До широкого распространения CORS это был популярный трюк для получения данных с другого домена. Суть: сайт вставляет на страницу тег <script src="https://other.com/data?callback=parser">. Сервер возвращает JavaScript-код, который вызывает глобальную функцию parser(...) с JSON-данными внутри. Поскольку <script> не блокируется SOP (скрипт выполнится), данные как бы «просачиваются» в вызов функции на стороне первого сайта. Недостатки JSONP – работает только для GET-запросов и несет риски (выполнение стороннего кода). В наши дни JSONP почти не используется, уступив место CORS, который поддерживает любые методы и не позволяет исполнять чужой код напрямую.

  • WebSockets. Интересно, что для WebSocket-соединений SOP в привычном виде не применяется. Страница из JS может попытаться подключиться к wss://another-domain.com/socket – браузер это разрешит. Но: при установке WS-соединения браузер всё равно отправляет Origin в handshake. Сервер WebSocket обязан сам проверить этот заголовок и решить, пускать ли этот origin. В противном случае злоумышленник мог бы в обход SOP наладить общение с приватным сервером. Таким образом, безопасность WS возложена на сервер: браузер доверяет ему и не блокирует попытки соединения.

  • Контроль загрузки ресурсов (CORP, COEP, COOP). Новые стандарты вводят дополнительные заголовки для усиления изоляции. Например, Cross-Origin Resource Policy (CORP) позволяет серверу объявить, что его ресурсы (скрипты, изображения и т.п.) не должны быть загружены на сторонних сайтах. Если изображение с CORP=same-site попытаться вставить через <img> на чужом сайте, браузер его заблокирует вовсе. Это помогает предотвратить атаки боковых каналов и утечку инфы через скрытое включение ресурсов. Cross-Origin Opener/Embedder Policy (COOP/COEP) – ещё более продвинутые заголовки, применяемые для изоляции контекстов (например, чтобы включить общий шаринг памяти, как SharedArrayBuffer, между вкладками одного сайта, нужно полностью изолировать их от посторонних). Эти темы выходят за рамки обзора, но их упоминание показывает, как развивается идея контроля взаимодействия между сайтами.

  • Private Network Access (PNA). Браузерные разработчики продолжают усиливать политику безопасности. Например, в 2022 г. появился механизм Private Network Access (PNA) – расширение CORS для защиты локальных сетей. Браузер Chrome одним из первых реализовал PNA: теперь если скрипт на сайте из интернета пытается обратиться к ресурсу в приватной сети (например, к вашему роутеру по адресу 192.168.0.1), перед собственно запросом браузер отправит специальный предварительный запрос с заголовком (Ссылка на источник информации):

 Access-Control-Request-Private-Network: true

локальный сервер (роутер) должен ответить заголовком:

Access-Control-Allow-Private-Network: true

иначе браузер блокирует соединение. Эта мера нацелена на предотвращение атак, при которых злоумышленники использовали браузер жертвы для несанкционированного доступа к устройствам в её локальной сети.

Что именно стоит зафиксировать после всего этого? Далее сформулируем ключевые тезисы и минимальный чек-лист.

Итоги и практические выводы

Same-Origin Policy уже почти 30 лет остается фундаментом безопасности в вебе. Благодаря SOP, наши браузеры изолируют вкладки и фреймы друг от друга, не давая сайтам похищать данные друг друга. Вместе с тем, современный веб невозможен без интеграции разных сервисов – и здесь как раз пригождается CORS. Этот механизм аккуратно расширяет SOP, позволяя безопасно обмениваться данными между доверенными доменами. Чтобы эффективно работать с CORS, разработчику нужно понимать, какие заголовки настроить на сервере и почему браузер блокирует тот или иной запрос. Подводя итог, отметим ключевые моменты:

  • SOP блокирует доступ скриптов к чужому контенту. Не доверяйте решениям, пытающимся отключить SOP – в современных браузерах это невозможно без потери безопасности.

  • CORS – инструмент, который находится в руках разработчика на стороне сервера. Правильно выставив заголовки (Origin, методы, заголовки, креденшлы), вы говорите браузеру: “этому запросу можно доверять”, и браузер пойдет навстречу.

  • При отладке проблем с CORS внимательно смотрите сообщения в консоли браузера – они подскажут, какого заголовка не хватает.

  • Всегда ограничивайте доступ по минимуму: указывайте конкретные источники вместо *, разрешайте только необходимые методы и заголовки. Это снизит шанс злоупотребления вашим API.

  • Безопасность эволюционирует: кроме CORS, изучайте и другие механизмы (CSRF-токены, SameSite cookies, CSP и т.д.), чтобы строить действительно защищенные приложения.

Понимая SOP и CORS, вы сможете уверенно работать с API, избегать надоедливых ошибок «Blocked by CORS» и защищать данные пользователей от большинства простых атак в вебе. Это обязательная основа для каждого веб-разработчика. 

Комментарии (2)


  1. itGuevara
    27.10.2025 07:18

    При отладке через браузерный JS иногда нужно как-то обходить CORS, например, при открытии локальных файлов. Можно ли настройкой браузера отключить? Или использовать специализированный браузер c игнорированием заданных CORS?


    1. Rsa97
      27.10.2025 07:18

      chrome --disable-web-security --user-data-dir=/path/to/user/profile
      

      Не забудьте предварительно прибить все инстансы хрома в памяти.