
В сети интернет достаточно информации на русском языке по поводу SOP и CORS, но введение в такие технологии как CORP, COEP и COOP показалось недостаточным (а кто-то может видеть эти аббревиатуры впервые). Поэтому решил написать статью по знакомству с CO* политиками.
В данной статье мы немного затронем SOP, подробнее поговорим про CORS, после чего перейдем к CORP, COEP и COOP.
Same Origin Policy
Согласно Same Origin Policy (далее SOP), два JavaScript-контекста могут обращаться к DOM друг друга, если у них совпадают источники (origin) – протокол, домен и порт. Данная политика выполняется браузером и зашита в нем по умолчанию.

SOP разрешает встраивать изображения с помощью тега <img>
, мультимедиа с помощью тега <video>
и JavaScript с помощью тега <script>
с сторонних источников. Эти внешние по отношению к нашему сайту ресурсы могут быть загружены и обработаны браузером: картинка будет отображаться, а скрипт загрузится и выполнится браузером. Однако JavaScript-код на нашей странице не сможет прочитать содержимое этих ресурсов, так как в данном случае браузер выдает opaque (непрозрачный) ответ.
Статей по SOP написано множество, поэтому на нем подробно не будем останавливаться. А в этой статье подробно описано, откуда взялся SOP (Same-Origin Policy). (Рекомендую ознакомиться для более лучшего понимания контекста).
С развитием технологий у разработчиков появилась необходимость доступа к ресурсам стороннего источника. Так появляется CORS (а перед этим JSONP, но он выходит за рамки данной статьи).
CO*
Небольшое отступление для лучшего понимания. В рамках данной темы часто используются обозначения cross-site и cross-origin.
Когда говорят "сайт" в широком смысле этого слова, обычно имеется в виду веб-сайт, то есть набор логически связанных между собой веб-страниц, расположенных в сети интернет. Всё, что было связано с взаимодействием между веб-сайтами до появления SOP имеет обозначение cross-site. Именно это значение сайта подразумевается в контексте уязвимостей XSS (Cross-Site Scripting) и CSRF (Cross-Site Request Forgery), так как эти уязвимости появились до появления термина origin.
Современное обозначение cross-site связано с терминологией сайта в контексте браузера. Термин "site" обозначает схему (scheme) URL-адресов (например, http://
) и TLD +1 (домен верхнего уровня (англ. top-level domain), плюс один домен второго уровня (см. картинку выше)). На русский язык он также переводится как "сайт".
Соответственно cross-site на просторах рунета дословно и переводится как "межсайтовый" или "кросс-сайтовый" и трактуется в зависимости от контекста: в современных технологиях - сайт - это схема и домен верхнего уровня; в уязвимостях, как правило, сайт как веб-сайт (современный origin).
С термином "origin" перевод немного интереснее. Если сам термин на русский язык переводится как "источник", то на просторах рунета "cross-origin" может переводиться как "междоменный" или "кросс-доменный", несмотря на то, что домен является частью источника (см. картинку выше). Могу предположить, что изменение домена автоматически ведет к изменению источника, поэтому такая трактовка допустима.
Все дальнейшие рассматриваемые политики начинаются с CO*, что является сокращением от cross-origin и означает политики для кросс-доменного взаимодействия.
CORS
Политика CORS (Cross Origin Resource Sharing) или Политика Кросс-Доменного Доступа должна была решить задачу ограничений кросс-доменного взаимодействия. Основной особенностью ее внедрения является обратная совместимость с работой браузера, так как те приложения, которые уже существовали, должны были сохранить свой функционал.
Кросс-доменные запросы, которые можно было отправить средствами HTML до внедрения CORS, должны были продолжить работать, как раньше. Такие запросы в терминологии CORS называются простыми. Любые другие кросс-доменные запросы, которые нельзя отправить средствами HTML, должны проходить специальную процедуру (preflight) перед своей отправкой браузером.
Простые запросы - это GET
, POST
, HEAD
запросы. У POST
-запросов, которые соответствуют HTML-формам, могут быть дополнительно изменены некоторые HTTP-заголовки на основе разметки формы.
Атрибут enctype
формы указывает значение заголовка Content-Type
, которое может иметь одно из следующих значений:
multipart/form-data
— по умолчанию используется при отправке HTML-форм, содержащих файлы;application/x-www-urlencoded
— используется при отправке HTML-форм в формате ключ-значение, в котором спецсимволы в именах и значениях полей формы подвергаются URL-кодированию;text/plain
— используется при отправке данных в формате ключ-значение без кодирования спецсимволов.
Также в простых запросах допустимо изменение заголовков Accept
, Accept-Language
, Content-Language
.

Также браузер будет автоматически подставлять заголовок Origin
с источником, который выполняет запрос (выставить его через JS-код не получится). В ответ на простые запросы политика CORS позволяет нам возвращать заголовки Access-Control-Allow-Origin
(далее ACAO) и Access-Control-Allow-Credentials
(далее ACAC), которые указывают браузеру разрешение на чтение ответов сервера определенным источникам.

ACAO должен содержать источник, которому разрешен доступ к кросс-доменному ресурсу. Если источник, с которого отправляется запрос и источник заголовка ACAO совпадают, то браузер передаст ответ в JavaScript-контекст SOP-источника отправителя. Если источники не будут совпадать, то браузер вернет opaque ответ.

В том случае, если необходимо разрешить любым источникам отправку кросс-доменных запросов к определенному ресурсу с доступом к ресурсу, то в качестве источника ACAO указывается значение звездочка (*
). При такой политике браузер разрешит JavaScript-коду прочитать ответ на запрос с любым источником, за исключением ситуации, когда для доступа к ресурсу требуются сессионные cookie.
Request.mode
Простые запросы также можно выполнять с использованием Fetch API, однако поведение браузера отличается в работе с отправкой кросс-доменных запросов через Fetch API и HTML-теги. Для лучшего понимания будем использовать свойство Request.mode из Fetch API.
Доступные значения данного атрибута:
same-origin — Если запрос сделан к другому origin в этом режиме, то это вызовет ошибку.
no-cors — Разрешает использование только
HEAD
,GET
илиPOST
методов и простых заголовков (т.е. разрешены только простые запросы).cors — Разрешает кросс-доменные запросы. В этом режиме браузер будет ожидать заголовок валидный ACAO от сервера. Если его не будет, браузер вернет ошибку CORS, но запрос всё равно будет отправлен. Браузер вернет opaque ответ.
navigate — Режим, поддерживающий навигацию. Значение navigate предназначено только для использования в HTML навигации. Запрос в этом режиме создаётся только во время навигации между страницами.
Примечание. Формально интерфейс Request не используется в HTML и является частью Fetch API, но режимы отправки запросов (Request.mode) так же применимы в контексте кросс-доменных запросов и для HTML.
По умолчанию все запросы, отправленные через HTML-теги, используют "no-cors mode" даже, если сервер возвращает CORS заголовки. В данном случае браузер не будет отправлять заголовок Origin
при запросе и вернет opaque ответ.
Чтобы браузер использовал "cors mode", необходимо явно указать это в HTML-теги с помощью атрибута crossorigin
. Например:
<script src="https://test.com/test.js" crossorigin></script>
Значение атрибута crossorigin="anonymous"
или атрибут без значения (crossorigin
) используется в HTML для указания браузеру, что элемент (например, <img>
, <video>
, <audio>
, <link>
и <script>
) должен быть загружен с включенной поддержкой кросс-доменных запросов (cors mode) без отправки учетных данных (Cookie, заголовки авторизации и т.д.).
В качестве эксперимента я использовал serveo, чтобы поднять временный сторонний сервер и отправлять на него запросы. Через консоль браузера на стартовой страничке создаем HTML-тег с нашим адресом и смотрим поведение браузера.


Появились Sec-Fetch-*
заголовки. Это заголовки метаинформации об источнике запроса, которые современные браузеры автоматически добавляют к запросам.
Sec-Fetch-Site со значением cross-site
говорит о том, что источником запроса является сторонний ресурс, а Sec-Fetch-Mode со значением no-cors
говорит о том, что отключается проверка CORS для запросов из разных источников (cross-origin). Итог: ответ непрозрачен (opaque), что означает , что его заголовки и текстовая часть недоступны для JavaScript. Также обратите внимание, что отсутствует заголовок запроса Origin
.
Если добавить атрибут crossorigin
для данного HTML-тега, то браузер будет ожидать ACAO в ответе сервера.



Использование же Fetch API напротив по умолчанию выполняется в cors mode. При таком запросе браузер будет подставлять заголовок Origin
и ожидать от сервера заголовок ACAO. Проверяем поведение браузера по тому же сценарию, только с использованием метода fetch()
в консоли браузера.

Как мы видим, браузер возвращает ошибку CORS, так как сервер не вернул корректный заголовок ACAO, но при этом запрос на сервер всё равно был отправлен. Так же появился заголовок запроса Origin
, но в значении null
(как в примере с картинкой в cors mode), так как запрос шел из начальной страницы браузера, а не с домена.

Чтобы fetch работал только с использованием SOP, т.е. с непрозрачными ответами, нужно явно это указать через поле mode в значении no-cors
.
Авторизационные данные
Мы уже знаем, что кросс-доменные запросы, отправленные через HTML-теги выполняются в no-cors mode. Еще одной их особенностью является автоматическая отправка Cookie (если они не защищены атрибутом SameSite), если такие есть для сайта (в контексте работы Cookie используются сайты, а не источники). Именно эта особенность лежит в основе уязвимости CSRF, т.е. подделки кросс-доменных запросов.
Суть в том, чтобы создать на подконтрольном ресурсе страницу, которая будет отправлять простой запрос на сторонний ресурс; заманить туда жертву, от лица которой выполнится простой запрос на изменение состояния (например, изменение почты, перевод денег и т.п.) с подстановкой Cookie:
<html>
<body>
<form action="https://vulnerable-website.com/email/change" method="POST">
<input type="hidden" name="email" value="pwned@evil-user.net" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
При посещении сайта http://evil.com
с таким кодом браузер выполнит POST
запрос на смену почты /email/change
на сайте https://vulnerable-website.com
на подконтрольную злоумышленнику. В этом случае, если у жертвы хранятся Cookie для данного сайта и выполняются все условия для атаки, то браузер автоматически подставляет Cookie жертвы в запрос, тем самым выполняя запрос от лица жертвы. В результате можно завладеть профилем жертвы (account takeover).
По этой причине плохой практикой является изменение состояния на сервере через GET
запросы. Например, если запрос на смену почты будет выглядеть подобным образом https://vulnerable-website.com/email/change?email=pwned@evil-user.net
, то CSRF можно выполнить просто через HTML-тег картинки:
<img src="//vulnerable-website.com/email/change?email=pwned@evil-user.net">
Если использовать для атаки Fetch API, то браузер не будет подставлять Cookie в кросс-доменный запрос, если явно не указать это в коде с использованием credentials: 'include'
, даже если Fetch запрос будет выполнен в no-cors mode. Однако, пока этот запрос считается простым, способ отправки кросс-доменного запроса не так важен. Как я и писал ранее, браузер поругается на ошибки CORS, но запрос все равно отправит.
// Простой POST-запрос с телом в формате form-data
fetch('https://bank.com/transfer', {
method: 'POST',
credentials: 'include', // отправка кук
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'amount=1000&to=attacker'
});
Тема CSRF и защита от нее обширна и выходит за рамки данной статьи, поэтому можете ознакомиться с ней самостоятельно.
С отправкой авторизованных запросов понятно, а что на счет чтения ответов на них?
Если мы используем fetch запрос выше из примера с CSRF, браузер заблокирует доступ к ответу, даже если сервер вернет Access-Control-Allow-Origin: *
или конкретный домен. Это происходит потому, что браузер ожидает новый для нас CORS заголовок сервера Access-Control-Allow-Credentials: true
.
Без этого заголовка браузер пользователя-жертвы откажется отправлять свои cookies, а значит, злоумышленник получит доступ только к неаутентифицированному контенту, который он мог бы с тем же успехом получить, перейдя непосредственно на целевой сайт.

Если говорить про кросс-доменные запросы, то, как мы уже знаем, чтобы получить доступ к ресурсу стороннего источника нужно использовать cors mode. Для этого нужно использовать уже знакомый нам атрибут crossorigin
. Чтобы получить доступ до авторизованного ответа сервера, нужно выставить в значении атрибута crossorigin="use-credentials"
.
В данном случае, как и с Fetch API, браузер будет ожидать оба заголовка ACAO и ACAС: true
Итоговая таблица по кросс-доменным простым запросам с использованием различных возможностей браузера:

Особенности
Отдельно рассмотрим случай, когда для любого запроса сервер одновременно выставляется следующие заголовки:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Современные браузеры будут блокировать передачу ответов, в которых указана такая политика, в JavaScript-контекст, из которых были сделаны запросы, и поднимать исключение.
Разработчики могут обходить такие блокировки, динамически выставляя значение ACAC из значения заголовка запроса Origin
, что является плохой практикой и приводит к уязвимостям.
Существует ещё одна особенность при использовании в заголовке ACAO. Если в ответе на запрос указан заголовок Set-Cookie
, а в заголовке ACAO указана *
, то браузер не выставит для пользователя данное значение cookie.
Preflight запрос
Ранее были даны характеристики простых запросов. Всё, что не попадает под описание простого запроса, можно считать "сложным" или "непростым". Например, если JS-код добавит произвольный HTTP-заголовок в запросе (тот же Authorization
) или отправит PUT
-запрос.
Для этого браузер предварительно отправит так называемый preflight запрос с HTTP-методом OPTIONS
, в котором будут перечислены метод и заголовки, добавленные или изменённые в основном запросе (исключением являются заголовки Accept
, Accept-Language,
Content-Language
, Content-Type
со стандартным для форм значениями, которые не попадут в preflight, если они будут изменены).

Основной запрос будет отправлен только в том случае, если будет получен ответ на preflight с политикой, явно разрешающей кросс-доменное взаимодействие: в нем будут явно указаны ресурс, с которого ожидается кросс-доменный запрос, заголовки, которые можно изменить или добавить, а также доступные методы. Ответ сервера будет содержать заголовки Access-Control-Allow-*
, где вместо *
будет содержаться та часть, которой приписывается политика. Например, Access-Control-Allow-Methods
описывает разрешенные HTTP методы, а Access-Control-Allow-Headers
описывает разрешенные HTTP заголовки.

Использование preflight запросов так же используется как защита от CSRF, так как основной запрос не выполняется автоматически с подстановкой Cookie, когда жертва посещает зловредный сайт. Однако не является хорошей практикой по защите от CSRF (см. доклад по данной теме).
Итоговая табличка по CORS:

Обычно представленных выше знаний достаточно многим специалистам, поэтому на тему SOP и CORS есть куча статей. Гораздо меньше внимания уделяется другим cross-origin политикам браузера.
CORP
Cross-Origin Resource Policy (CORP) — это настройка безопасности, которая говорит браузеру: «Эти файлы (картинки, скрипты, стили) можно использовать только на моём сайте или источнике. Другие сайты не могут их встраивать!». Данная политика является настройкой изоляции через браузер и работает только в no-cors mode. Это значит, что ограничения будут касаться только тех ресурсов, которые были загружены и отрендерены без использования CORS, например, с помощью тегов script
и img
.
Вы так же можете выполнить запрос через fetch, но переведя его в no-cors mode, запрос выполнится, но доступ к ответу вы не получите.
fetch('http://corpsite.ru/someResource', {mode: "no-cors"})
Более формальное определение будет звучать так: "Заголовок ответа HTTP Cross-Origin-Resource-Policy (CORP) указывает, что браузер должен блокировать cross-site или cross-origin запросы к данному ресурсу".
Сервер отправляет специальный заголовок Cross-Origin-Resource-Policy
. с следующими значениями:
same-origin — ресурс можно использовать только на том же источнике (например, example.com→ example.com).
same-site — разрешает использование на поддоменах (вспоминаем, что такое site в контексте браузеров)
(например, blog.example.com → example.com).cross-origin — разрешает встраивать ресурс на любых источниках.

Для чего это нужно? Сторонний источник не сможет встроить ваши картинки, видео или скрипт без разрешения (например, если у вас свой CDN). Так же, если у вас есть приватные файлы (например, PDF-документы), их нельзя будет открыть на сторонних сайтах. Помимо этого это надежная защита от атак, подобных Spectre, поскольку позволяет браузерам блокировать данный ответ до того, как он попадет в процесс злоумышленника.
Отличия от CORS:
CORS управляет доступом к ресурсам через JavaScript (например, через fetch).
CORP управляет встраиванием ресурсов в HTML (теги
<img>
,<script>
,<iframe>
).
Получается, что для кросс-доменного запроса применение политики зависит от Request.mode:
Если запрос выполняется в cors mode (например, обычный fetch запрос), то браузер будет проверять CORS заголовки и игнорировать CORP
Если запрос выполняется в no-cors mode (например, HTML-тег картинки без атрибута
crossorigin
), то браузер будет проверять CORP заголовок и игнорировать CORS.
COEP
Cross-Origin Embedder Policy (COEP) говорит браузеру: «Встраивай на мою страницу только те картинки, скрипты и стили (в общем ресурсы), доступ к которым явно разрешен.»
Если ваш сайт включает COEP, браузер проверяет все встраиваемые ресурсы (изображения, скрипты, видео и т.д.):
Те, что загружены с вашего источника (например, ваш-сайт.com → ваш-сайт.com/logo.png) — разрешены.
Те, что загружены с других источников, должны явно разрешить встраивание через CORS или CORP.
Если ресурс не проходит проверку, браузер его блокирует.

Для активации ограничения необходимо добавление заголовка ответа сервера Cross-Origin-Embedder-Policy
с указанием директивы require-corp
. Это значение является единственным доступным значением рассматриваемого заголовка, кроме unsafe-none
, которое является значением по умолчанию.
Работа браузера с CORP и COEP хорошо показана в данном видео.
COOP
Как и COEP это политика источника ресурса. Когда вы открываете ссылку в новой вкладке (например, через target="_blank"
), браузер обычно сохраняет связь между старой и новой вкладкой. Это позволяет:
Родительской вкладке получить доступ к
window
дочерней (например, черезwindow.opener
).Дочерней вкладке взаимодействовать с родительской (например, менять её URL).
Cross-Origin-Opener-Policy (COOP) разрывает эту связь, чтобы злоумышленники не могли использовать её для атак.
На сервере выставляется заголовок Cross-Origin-Opener-Policy
с возможными значениями:
unsafe-none (по умолчанию):
Связь между вкладками разрешена.
Нет защиты.
same-origin:
Вкладка может общаться только с вкладками того же источника.
Все cross-origin вкладки изолируются.
same-origin-allow-popups:
Вкладка может общаться только с вкладками, которые она сама открыла (и теми, что с того же источника).
Browsing context
Контекст просмотра (Browsing context) - это среда, в которой браузер отображает документ. В современных браузерах это обычно вкладка, но это может быть окно, всплывающее окно, веб-приложение или даже часть страницы, такая как фрейм или iframe.
Каждый контекст просмотра имеет определённый источник, источник текущего активного документа и историю, которая содержит все отображённые документы в соответствующем порядке.
Взаимодействие между контекстами просмотра очень ограничено. Между контекстами просмотра из одного источника может быть открыт и использован BroadcastChannel.
Контекст просмотра может быть частью группы контекста просмотра (browser context group), которая представляет собой набор контекстов просмотра, имеющих общий контекст, такой как история, куки, механизмы хранения и так далее. Контексты просмотра внутри группы сохраняют ссылки друг на друга и поэтому могут просматривать глобальные объекты друг друга и отправлять друг другу сообщения.

COOP позволяет документу верхнего уровня разорвать связь между своим окном и любыми другими в группе контекста просмотра (например, между всплывающим окном и его открывателем), предотвращая любой прямой доступ к DOM между ними. Для этого документ перемещается в новую группу контекста просмотра. Безопасность такого подхода обусловлена тем, что браузеры обычно создают группы контекста просмотра путем порождения нового процесса, что гарантирует, что документ не будет помещен в память, к которой имеет доступ контролируемый злоумышленником документ.
Браузер применяет COOP, обращаясь к заголовку Cross-Origin-Opener-Policy в каждой высокоуровневой навигации, сравнивая его значение (обычно same-origin) между документом, инициировавшим навигацию (переходом по ссылке или открытием всплывающего окна), и целью навигации. Если значения COOP одинаковы, а происхождение документов совпадает, браузер не будет создавать новую группу контекста просмотра, позволяя документам взаимодействовать друг с другом. В противном случае, если хотя бы один из документов задает COOP, браузер создаст новую контекстную группу просмотра, разрывая связь между документами.

Cross-origin изоляция
Чтобы перейти в состояние cross-origin изоляции, основной документ должен содержать следующие HTTP-заголовки:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Браузер сравнивает значения заголовка COEP, если COOP - same-origin, гарантируя, что документы с COOP same-origin должны иметь такой же COEP, чтобы присутствовать в одной и той же группе контекста просмотра браузера.
Поскольку COOP определяется в терминах групп контекста просмотра, он не применяется к iframe'ам; браузер игнорирует COOP для документов, которые не являются документами верхнего уровня (top-level document), и позволяет iframe'ам получать доступ к своим cross-origin предкам, даже если эти предки устанавливают COOP.
Итого
Мы с вами прошли путь от появления понятия "Origin" до политик безопасности cross-origin браузера. Надеюсь, вы узнали что-то новое, и информация была проста для восприятия.
В качестве заключения можно запомнить cross-origin политики по такой картинке:

Больше информации публикую в моем тг канале:
