# Same-origin Policy и Same-Site

В предыдущих статьях мы несколько раз упоминали термин `"Same-Origin"`. Это крайне важный термин как в мире фронтенда, так и в кибербезопасности. `Same-Origin Policy` в браузерах имеет важное значение как для разработки, так и для защиты от атак.

Кроме того, есть несколько терминов, которые часто путают, таких как `Host` и `Site`. Например, XS в XSS означает `"Cross-Site"`, и CS в CSRF также означает `"Cross-Site"`. Так в чем же разница между `Origin` и `Site`? Чем они отличаются от `Host`?

### Что такое Origin и Site? Как их различать?

Начнем с простого и несколько неточного объяснения, а затем мы шаг за шагом уточним детали.

`Origin` состоит из схемы, порта и хоста. В совокупности они формируют `Origin`.

Например, если у нас есть URL, такой как `https://example.com/example_path`, его компоненты:

* Схема: https
* Порт: 443 (порт по умолчанию для https)
* Хост: example.com

Таким образом, его `Origin` — это `https://example.com`. Как видно, часть пути `/example_path` не влияет на `Origin`, а порт в данном случае подразумевается по умолчанию как 443.

`Same-Origin` означает, что происхождение двух URL должно быть одинаковыми. Например:

1. `https://example.com/example_path` и `https://example.com/path/example` имеют одинаковый `Origin`, потому что схема, порт и хост одинаковы, а путь не влияет на результат.
2. `https://example.com` и `http://example.com` не являются `Same-Origin`, потому что схемы различаются.
3. `http://example.com` и `http://example.com:8080` не являются `Same-Origin`, потому что порты различаются.
4. `https://example` и `https://sub-domain.example.com` также не являются `Same-Origin`, потому что хосты различаются.

Из приведенных выше примеров видно, что условия для `Same-Origin` довольно строгие. В основном, за исключением пути, все остальные части должны быть одинаковыми, чтобы считаться `Same-Origin`.

Теперь давайте посмотрим, что такое `Site`. `Site` учитывает меньше элементов по сравнению с `Origin`.

Он рассматривает только схему и хост, игнорируя порт. Определение того, что два URL-адреса являются `Same-Site`, более простое, поскольку хост не обязательно должен быть абсолютно одинаковым.

Если это поддомен, он все равно считается `Same-Site`.

Например:

1. `https://example.com/example_path` и `https://example.com/path/example` являются `Same-Site`, потому что схема и хост одинаковы.
2. `https://example.com` и `http://example.com` не являются `Same-Site`, потому что схемы различаются.
3. `http://example.com` и `http://example.com:8080` являются `Same-Site`, потому что порт не влияет на результат.
4. `https://example.com` и `https://sub-domain.example.com` являются `Same-Site`, потому что оба домена - `example.com` и `sub-domain.example.com` находятся под одним родительским доменом, `example.com`.
5. `https://sub-domain-1.example.com` и `https://sub-domain-2.example.com` также являются `Same-Site`, потому что оба домена - `sub-domain-1.example.com` и `sub-domain-2.example.com`находятся под одним родительским доменом -`example.com`.

По сравнению с `Same-Origin`, очевидно, что `Same-Site` более простой. Разные порты считаются `Same-Site`, и если хост принадлежит одному и тому же родительскому домену, он обычно считается `Same-Site`.

Однако, как я упоминал в начале, хоть приведенные выше определения верны, в большинстве случаев, они не являются точными. Давайте обратимся к спецификации, чтобы увидеть исключения.

### Подробное изучение Same-origin

В разделе [7.5 Origin](https://html.spec.whatwg.org/multipage/browsers.html#origin) спецификации HTML вы можете найти полное определение. Давайте рассмотрим объяснение `Origin` в спецификации:

> Origins are the fundamental currency of the web's security model. Two actors in the web platform that share an origin are assumed to trust each other and to have the same authority. Actors with differing origins are considered potentially hostile versus each other, and are isolated from each other to varying degrees.

Содержание здесь очень понятное. В начале объясняется, что если два сайта имеют одинаковое происхождение, это означает, что эти два сайта доверяют друг другу. Однако если у них разное происхождение, они будут изолированы и ограничены.

Далее спецификация делит Origins на два типа: **Непрозрачное происхождение (opaque origin)** и **Кортеж происхождения (tuple origin)**

`Непрозрачное происхождение` можно рассматривать как происхождение, которое появляется только в особых случаях. Например, когда я открываю веб-страницу на своей локальной машине, URL будет иметь вид «file:///...». В этом случае при отправке запроса внутри веб-страницы в качестве `Origin` будет использоваться непрозрачный `Origin`, то есть `«null»`.

Кортежное происхождение встречается чаще всего, и именно этот тип происхождения нас больше всего волнует. В документации говорится, что кортеж происхождения включает в себя:

1. схему (ASCII-строка).
2. хост (хост).
3. порт (null или 16-битное целое число без знака).
4. домен (null или домен). Null, если не указано иное.

Вы можете задаться вопросом, зачем нужны хост и домен. Мы обсудим это позже.

Кроме того, в спецификации также описан алгоритм определения того, являются ли два `Origins` A и B `Same-Origin`:

1. Если A и B — одно и то же непрозрачное происхождение, то возвращаем true.
2. Если A и B являются кортежами и их схемы, хосты и порты идентичны, то возвращается true.
3. Вернуть false.

Либо оба `Origins` являются одним и тем же непрозрачным `Origin`, либо их схемы, хосты и порты идентичны, чтобы считаться `Same-Origin`. Помимо одинакового происхождения, в спецификации вы также встретите еще один термин, называемый `"Same Origin-Domain"`, который будет объяснен позже.

Как я уже упоминал ранее, `Same-Origin` — это строгое ограничение. Например, для URL `"https://example.com/api"` поскольку при определении происхождения не учитывается путь, его `Origin` будет `"https://example.com"`. Это означает, что любой сайт с таким же происхождением должен иметь URL, начинающийся с `"https://example.com/*"`, чтобы считаться таким же по происхождению.

Хотя `"https://example.com"` и `"https://sub-domain.example.com"` связаны с одним доменом, они не являются `Same-Origin`, потому что хосты различаются.

Запомните это, так как это важно.

Подробное изучение `Origin` и `Same-Origin` в спецификации, по сравнению с "неточным утверждением", упомянутым в начале, добавляет `Opaque Origin`, `Same Origin-Domain` и `Tuple Origin`.

Наконец, позвольте мне упомянуть еще одну вещь. Когда я говорю, что `Origin` `"https://example.com/api"` равен `"https://example.com"`, более точное утверждение будет: "The serialized form of the origin of `'https://example.com/api'` is `'https://example.com'`".

Это потому, что ранее упоминалось, что происхождение на самом деле является кортежем, представленным как `(https, example.com, null, null)`, и оно становится `https://example.com` при сериализации в строку. Мне кажется, что сериализованная форма кортежа легче читается по сравнению с его представлением в виде кортежа. Поэтому, когда оба варианта могут передать схожую информацию, я предпочитаю последний подход.

### Подробное изучение Same-Site

В той же спецификации есть определение `Site`, которое гласит:

> A site is an opaque origin or a scheme-and-host.

Итак, сайт может быть непрозрачным источником или схемой и хостом.

В спецификации, помимо термина `"Same-Site"`, можно найти еще один термин, называемый `"Schemelessly Same-Site"`. Разница между ними также очевидна: Same-Site учитывает схему, а `Schemelessly Same-Site` не учитывает схему.

Поэтому при определении того, являются ли два `Origins`, A и B - `Same-Site`, алгоритм выглядит следующим образом:

> Two origins, A and B, are said to be same site if both of the following statements are true:
>
> * A and B are schemelessly same site
> * A and B are either both opaque origins, or both tuple origins with the same scheme

Если A и B являются `Same-Site`, то либо они оба `Opaque Origins`, либо у них одинаковая схема и они являются `Schemelessly Same-Site`.

Таким образом, понятие `"Same-Site"` зависит от схемы. URL с разными схемами, например http и https, никогда не считаются `Same-Site`, но они могут быть `Schemelessly Same-Site`.

Здесь есть немного истории. Когда `Same-Site` был впервые введен, он не учитывал схему. Позже схема была принята во внимание.

В RFC 2016: Same-Site Cookies можно увидеть, что определение `Same-Site` не включало схему. Таким образом, в то время `https://example.com` и `http://example.com` считались `Same-Site`.

Только в июне 2019 года начались обсуждения о том, следует ли включать схему в рассмотрение. Для получения более подробной информации смотрите: <https://github.com/w3c/webappsec-fetch-metadata/issues/34>.

В то время спецификация `Same-Site` не была определена в HTML-спецификации, которую мы видим сегодня, а в другой спецификации URL. Поэтому обсуждение было перенесено туда: [Consider introducing a "same-site" concept that includes scheme](https://github.com/whatwg/url/issues/448). Затем в сентябре 2019 года был представлен этот PR: [Tighten 'same site' checks to include 'scheme'](https://github.com/whatwg/url/pull/449), который официально включил схему в спецификацию. `Same-Site` был определен как `"onsidering the scheme"`, и был введен новый термин для обозначения игнорирования схемы: `Schemelessly Same-Site`.

Затем, через два месяца, соответствующая спецификация была перенесена из URL в HTML. Вы можете ознакомиться с этими двумя PR:

[Let HTML handle the "same site" definition #457](https://github.com/whatwg/url/pull/457), [Define (schemelessly) same site for origins #5076](https://github.com/whatwg/html/pull/5076).

Спецификации — это одно, но иногда браузеры не сразу успевают за изменениями. Так какова текущая реализация в браузерах?

В ноябре 2020 года Chrome опубликовал статью: [Schemeful Same-Site](https://web.dev/schemeful-samesite/), в которой указывалось, что в то время разные схемы все еще считались `Same-Site`. Но из [Chrome platform status: Feature: Schemeful same-site](https://chromestatus.com/feature/5096179480133632) видно, что Chrome начал учитывать схему с версии 89.

Что касается Firefox, из статуса этого вопроса: [\[meta\] Enable cookie sameSite schemeful](https://bugzilla.mozilla.org/show_bug.cgi?id=1651119) кажется, что это поведение еще не является стандартным. Если не настроено специально, разные схемы все еще считаются `Same-Site`.

После изучения истории давайте посмотрим, как определяется `Schemelessly Same-Site`:

<figure><img src="/files/dclTM02Q1DA2JcVpF38W" alt=""><figcaption></figcaption></figure>

Ключевой момент выше — это новый термин: `"Registrable Domain",` который используется для сравнения двух хостов, чтобы определить, являются ли они `Same-Site`.

Определение `Registrable Domain` содержится в другой спецификации URL: [спецификация URL](https://url.spec.whatwg.org/#host-registrable-domain).

> A host's registrable domain is a domain formed by the most specific public suffix, along with the domain label immediately preceding it, if any.

Здесь упоминается новый термин: `"Public Suffix"`.

Начнем с примера, чтобы лучше это понять.&#x20;

Registrable domain `sub-domain.example.com` будет `example.com`, а Registrable domain `example.com` также будет `example.com`.

Однако Registrable domain `bob.github.io` не `github.io`, а скорее `bob.github.io`.

Почему так? Позвольте мне кратко объяснить.

Если бы у нас не было понятий "Registrable Domain" и "Public Suffix", то определение `Same-Site` было бы таким, как упоминалось ранее: `example.com` и `sub-domain.example.com` считались `Same-Site`, и в этом не было бы проблем.

Но если бы это было так, то `bob.github.io` и `alice.github.io` также считались `Same-Site`.

Подождите, это ведь проблема?

Да, это проблема. `github.io` — это сервис, предоставляемый GitHub, и у каждого пользователя GitHub есть свой собственный поддомен. Однако GitHub не хочет, чтобы `bob.github.io` мешал `alice.github.io`, потому что это на самом деле два совершенно независимых веб-сайта, в отличие от `example.com` и `sub-domain.example.com`, которые принадлежат мне.

Поэтому возникла концепция `Public Suffix`. Это список, который ведется вручную и содержит "list of domains that should not be considered as the same websit". Позвольте привести несколько примеров:

1. github.io
2. com.tw
3. s3.amazonaws.com
4. azurestaticapps.net
5. herokuapp.com

Таким образом, после обращения к этому списку браузер распознает, что `bob.github.io` и `alice.github.io` не связаны и не являются `Same-Site`. Существует также конкретный термин для этого — `eTLD (effective Top-Level-Domain)`. Для получения более подробной информации вы можете обратиться к статье: [How to determine if two domains have the same owner?](https://blog.kalan.dev/2021-11-09-url-and-samesite/)

Как упоминалось выше, поскольку `github.io` включен в список `Public Suffix`, `Registrable Domain` `bob.github.io` — это `bob.github.io`, а `Registrable Domain` `alice.github.io` — это `alice.github.io`.

Таким образом, определение `Same-Site`, которое мы изначально упоминали, неверно. Два хоста могут казаться принадлежащими одному и тому же родительскому домену, но это не обязательно означает, что они являются `Same-Site`. Это также зависит от того, включены ли они в список `Public Suffix`.

`bob.github.io` и `alice.github.io` не являются `Same-Site`, потому что их `Registrable Domains` различны.

`sub-domain-1.example.com`, `example.com` и `sub-domain-2.example.com` — все они `Same-Site`, потому что их `Registrable Domain` — `example.com`.

Спецификация включает более ясную таблицу для справки. Пожалуйста, обратите на нее внимание:

<figure><img src="/files/T4XmRzhTxFsgoEUQToNt" alt=""><figcaption></figcaption></figure>

Наконец, давайте подведем итоги по поводу `Same-Site`:

1. Существуют `Same-Site` и `Schemelessly Same-Site`, при этом первый используется чаще.
2. Чтобы определить, являются ли два хоста `Same-Site`, необходимо рассмотреть `Registrable Domain`.
3. Для определения `Registrable Domain` нужно обратиться к списку `Public Suffix`.
4. Даже если два хоста кажутся принадлежащими одному и тому же родительскому домену, они могут не быть `Same-Site` из-за наличия `Public Suffix`.
5. `Same-Site` не учитывает порт, поэтому `http://sub-domain.example.com:8888` и `http://example.com` являются `Same-Sate`.

### Same-Origin и Same-Site

`Same-Origin` определяется по:

1. Scheme
2. Port
3. Host

А `Same-Site` определяется по:

1. Scheme
2. Host (Registrable Domain)

Если два веб-сайта имеют `Same-Origin`, то они обязательно должны быть `Same-Site`, поскольку критерии для определения `Same-Origin` строже.

Основные различия между ними:

1. `Same-Origin` учитывает порт, в то время как `Same-Site` — нет.
2. `Same-Origin` учитывает хост, в то время как `Same-Site` учитывает `Registrable Domain`.

### Магический document.domain

При изучении спецификации источника упоминается магический атрибут `"domain"`, назначение которого не совсем ясно. В спецификации источника также упоминается нечто под названием "одинаковый источник-домен" (same origin-domain), и есть зеленая заметка, которая касается этого:

<figure><img src="/files/xfDDeQeQhyWCSbnoZfgI" alt=""><figcaption></figcaption></figure>

В ней говорится, что источник неизменяем, за исключением атрибута `domain`, который можно изменить с помощью `document.domain`. В спецификации есть раздел [7.5.2 Relaxing the same-origin restriction](https://html.spec.whatwg.org/multipage/origin.html#relaxing-the-same-origin-restriction) который объясняет это. Вот небольшой отрывок:

> (document.domain) can be set to a value that removes subdomains, to change the origin's domain to allow pages on other subdomains of the same domain (if they do the same thing) to access each other. This enables pages on different hosts of a domain to synchronously access each other's DOMs.

Чтобы помочь всем понять, давайте проведем демонстрацию. Я изменил файл /etc/hosts на своем локальном компьютере, добавив следующее содержимое:

```
127.0.0.1   alice.example.com
127.0.0.1   bob.example.com
```

Теперь оба этих URL будут подключаться к локальному компьютеру. Затем я запустил простой HTTP-сервер и создал базовую HTML-страницу, работающую на localhost:5555.

{% code overflow="wrap" %}

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
    <h1></h1>
    <h2></h2>
    <button onclick="load('alice')">load alice iframe</button>
    <button onclick="load('bob')">load bob iframe</button>
    <button onclick="access()">access iframe content</button>
    <button onclick="update()">update domain</button>
    <br>
    <br>
</body>
<script>
    const name = document.domain.replace('.example.com', '');
    document.querySelector('h1').innerText = name;
    document.querySelector('h2').innerText = Math.random();
    
    function load(name) {
        const iframe = document.createElement('iframe');
        iframe.src = 'http://' + name + '.example.com:5555';
        document.body.appendChild(iframe);
    }
    
    function access() {
        const win = document.querySelector('iframe').contentWindow;
        alert('secret:' + win.document.querySelector('h2').innerText);
    }
    
    function update() {
        document.domain = 'example.com';
    }
</script>
</html>
```

{% endcode %}

Страница имеет три функции:

1. Загрузка iframe
2. Чтение данных из DOM iframe
3. Изменение document.domain

Давайте начнем с открытия `http://alice.example.com:5555` и затем загрузим iframe с `http://bob.example.com:5555`. Затем нажмите "access iframe content" на странице Алисы.

Вы увидите сообщение об ошибке в консоли, которое гласит:

<figure><img src="/files/l5s5VdSS6VnA3S0Eo8nS" alt=""><figcaption></figcaption></figure>

Вы увидите сообщение об ошибке в консоли, которое гласит:

> Uncaught DOMException: Blocked a frame with origin "<http://alice.example.com:5555>" from accessing a cross-origin frame.

Это происходит потому, что хотя alice и bob имеют `Same-Site`, они не являются `Same-Origin`. Чтобы iframe мог получить доступ к содержимому DOM, он должен находиться на одном и том же источнике.

Далее оба, alice и bob, нажимают "update domain" на своих страницах, а затем снова нажимают "access iframe content":

<figure><img src="/files/5aHMz8iuY9NjjRNPATpk" alt=""><figcaption></figcaption></figure>

На этот раз вы увидите, что мы успешно получили данные со страницы Боба и изменили http\://`alice.example.com:5555` и `http://bob.example.com:5555` с `Cross-Origin` на `Same-Origin`.

Эту технику не могут использовать любые две веб-страницы. В основном, только веб-сайты c `Same-Site` могут ее использовать, и также существует множество проверок во время настройки:

<figure><img src="/files/zqHU9egvhfDMO7SPwuAM" alt=""><figcaption></figcaption></figure>

Принимая github.io в качестве примера, если alice.github.io выполнит `document.domain = 'github.io'`, в консоли будет выдана ошибка:

> Uncaught DOMException: Failed to set the 'domain' property on 'Document': 'github.io' is a top-level domain.

Почему изменение `document.domain` делает две страницы Same-Origin? Строго говоря, это не одинаковый источник, а одинаковый источник-домен. В спецификации, связанной с `document`, упоминается, что некоторые проверки основаны на одинаковом источник-домене, а не на одинаковом источнике.

Итак, как мы определяем, являются ли два источника одинаковым источник-доменом? Давайте посмотрим, что говорит спецификация:

> 1. If A and B are the same opaque origin, then return true.
>
> > 2) If A and B are both tuple origins, run these substeps:
> > 3) If A and B's schemes are identical, and their domains are identical and non-null, then return true.
> > 4) Otherwise, if A and B are same origin and their domains are identical and null, then return true.
> > 5) Return false.

Если A и B имеют одинаковую схему, и их свойства домена идентичны и не равны null, то вернуть true. В противном случае, если A и B — это одинаковый источник и их домены идентичны и равны null, то вернуть true. В противном случае вернуть false.

Вот несколько интересных моментов:

* Обе веб-страницы должны либо не иметь установленного домена, либо иметь установленный одинаковый домен, чтобы потенциально вернуть true (это важно).
* Если домен установлен, проверка одинакового источник-домена не учитывает порт.

`document.domain` используется для изменения свойства домена в кортеже источника.

В приведенном выше примере обе наши веб-страницы, `http://alice.example.com:5555` и `http://bob.example.com:5555`, изменяют свой домен на `example.com`, поэтому они становятся одинаковым источник-доменом.

Теперь давайте рассмотрим три интересных сценария.

### Сценарий 1: Одностороннее изменение

Если `https://alice.example.com` выполнит `document.domain = 'example.com'`, а затем встроит `https://example.com` в iframe, они все равно не будут `Same Origin-Domain`, потому что страница Алисы имеет установленное свойство домена, а страница example.com — нет. Страница example.com также должна выполнить `document.domain = 'example.com'`, чтобы они стали `Same Origin-Domain`.

### Сценарий 2: Исчезающий порт

`http://alice.example.com:1234` и `http://alice.example.com:4567` считаются `Cross-Origin`, потому что у них разные порты. Однако, если обе страницы выполнят `document.domain = 'alice.example.com'`, они станут `Same Origin-Domain` и смогут получить доступ к DOM друг друга, потому что порт не учитывается.

### Сценарий 3: Я не тот, кем был раньше

Предположим, `http://alice.example.com` встраивает себя в iframe, iframe и оригинальная страница явно являются `Same-Origin` и могут получить доступ к DOM друг друга. Однако, если я выполню `document.domain = 'alice.example.com'` на странице, страница установит атрибут домена, но страница внутри iframe не имеет установленного атрибута домена. Поэтому они становятся разными `Origin-Domains`.

### The fading and exit of document.domain

Использование этой техники для ослабления ограничения `Same-Origin` существует уже довольно давно, и она не была удалена для совместимости с ранними поведениями. Я предполагаю, что многие веб-страницы использовали эту технику для доступа к страницам одного сайта, но с междоменным доступом раннее.

Однако этот подход явно рискован. Например, если поддомен имеет уязвимость XSS, ее можно использовать для расширения воздействия. В статье 2016 года @fin1te под названием "[An XSS on Facebook via PNGs & Wonky Content Types](https://whitton.io/articles/xss-on-facebook-via-png-content-types/)" эта техника была использована для успешного выполнения XSS с поддомена на [www.facebook.com](http://www.facebook.com), увеличивая воздействие уязвимости.

Из-за проблем с безопасностью Chrome опубликовал 11 января 2022 года статью в своем блоге: "[Chrome will disable modifying document.domain to relax the same-origin policy](https://developer.chrome.com/blog/immutable-document-domain)". В статье объясняется, что начиная с версии Chrome 101 поддержка изменения `document.domain` будет прекращена.

Оригинальное поведение можно заменить на `postMessage` или `API Channel Messaging`, но это требует написания большего объема кода. В конце концов, это не так удобно, как прямое манипулирование DOM. Если веб-страница хочет продолжать использовать функциональность изменения `document.domain`, ей необходимо включить в заголовок ответа `Origin-Agent-Cluster: ?0`.

Статья также включает связанную дискуссию по этому изменению: [Deprecating `document.domain` setter](https://github.com/w3ctag/design-reviews/issues/564)

## Заключение

`Same-Origin Policy` — это механизм защиты браузера, который гарантирует, что только веб-страницы с `Same-Origin` могут получать доступ к данным друг друга, чтобы избежать проблем с безопасностью. Поэтому важно понимать определение источника, чтобы определить, принадлежат ли две веб-страницы одному и тому же источнику.

После введения важных концепций источника и сайта мы постепенно столкнемся с двумя связанными терминами: CSRF (межсайтовая подделка запросов) и CORS (междоменный доступ к ресурсам).

References:

1. [HTML spec](https://html.spec.whatwg.org/multipage/origin.html#origin)
2. [URL spec](https://url.spec.whatwg.org/#host-registrable-domain)
3. [如何判斷兩個網域的擁有者是否相同？](https://blog.kalan.dev/2021-11-09-url-and-samesite/)
4. [Chrome will disable modifying document.domain to relax the same-origin policy](https://developer.chrome.com/blog/immutable-document-domain/)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wr3dmast3r.gitbook.io/client-side-fundamental/glava-4-mezhsaitovye-ataki/same-origin-policy-i-same-site.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
