Политика «Одинакового источника» (Same Origin) ограничивает доступ окон и фреймов друг к другу.
Идея заключается в том, что если у пользователя открыто две страницы: john-smith.com
и gmail.com
, то у скрипта со страницы john-smith.com
не будет возможности прочитать письма из gmail.com
. Таким образом, задача политики «Одинакового источника» – защитить данные пользователя от возможной кражи.
Политика "Одинакового источника"
Два URL имеют «одинаковый источник» в том случае, если они имеют совпадающие протокол, домен и порт.
Эти URL имеют одинаковый источник:
http://site.com
http://site.com/
http://site.com/my/page.html
А эти – разные источники:
http://www.site.com
(другой домен:www.
важен)http://site.org
(другой домен:.org
важен)https://site.com
(другой протокол:https
)http://site.com:8080
(другой порт:8080
)
Политика «Одинакового источника» говорит, что:
- если у нас есть ссылка на другой объект
window
, например, на всплывающее окно, созданное с помощьюwindow.open
или наwindow
из<iframe>
и у этого окна тот же источник, то к нему будет полный доступ. - в противном случае, если у него другой источник, мы не сможем обращаться к его переменным, объекту
document
и так далее. Единственное исключение – объектlocation
: его можно изменять (таким образом перенаправляя пользователя). Но нельзя читатьlocation
(нельзя узнать, где находится пользователь, чтобы не было никаких утечек информации).
Доступ к содержимому ифрейма
Внутри <iframe>
находится по сути отдельное окно с собственными объектами document
и window
.
Мы можем обращаться к ним, используя свойства:
iframe.contentWindow
ссылка на объектwindow
внутри<iframe>
.iframe.contentDocument
– ссылка на объектdocument
внутри<iframe>
, короткая запись дляiframe.contentWindow.document
.
Когда мы обращаемся к встроенному в ифрейм окну, браузер проверяет, имеет ли ифрейм тот же источник. Если это не так, тогда доступ будет запрещён (разрешена лишь запись в location
, это исключение).
Для примера давайте попробуем чтение и запись в ифрейм с другим источником:
<iframe src="https://example.com" id="iframe"></iframe>
<script>
iframe.onload = function() {
// можно получить ссылку на внутренний window
let iframeWindow = iframe.contentWindow; // OK
// ...но при попытке получить доступ к document страницы
let doc = iframe.contentDocument; // ...получим null
// также мы не можем прочитать URL страницы в ифрейме
try {
// нельзя читать из объекта Location
let href = iframe.contentWindow.location.href; // ОШИБКА
} catch(e) {
alert(e); // Security Error
}
// ...но можно писать в него (и загрузить что-то другое в ифрейм)!
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // уберём обработчик, чтобы не срабатывал после изменения location
};
</script>
Код выше выведет ошибку или null
для любых операций, кроме:
- Получения ссылки на внутренний объект
window
изiframe.contentWindow
- Изменения
location
.
С другой стороны, если у ифрейма тот же источник, то с ним можно делать всё, что угодно:
<!-- ифрейм с того же сайта -->
<iframe src="/" id="iframe"></iframe>
<script>
iframe.onload = function() {
// делаем с ним что угодно
iframe.contentDocument.body.prepend("Привет, мир!");
};
</script>
iframe.onload
и iframe.contentWindow.onload
Событие iframe.onload
– по сути то же, что и iframe.contentWindow.onload
. Оно сработает, когда встроенное окно полностью загрузится со всеми ресурсами.
…Но iframe.onload
всегда доступно извне ифрейма, в то время как доступ к iframe.contentWindow.onload
разрешён только из окна с тем же источником.
Окна на поддоменах: document.domain
По определению, если у двух URL разный домен, то у них разный источник.
Но если в окнах открыты страницы с поддоменов одного домена 2-го уровня, например john.site.com
, peter.site.com
и site.com
(так что их общий домен site.com
), то можно заставить браузер игнорировать это отличие. Так что браузер сможет считать их пришедшими с одного источника при проверке возможности доступа друг к другу.
Для этого в каждом таком окне нужно запустить:
document.domain = 'site.com';
После этого они смогут взаимодействовать без ограничений. Ещё раз заметим, что это доступно только для страниц с одинаковым доменом второго уровня.
Ифрейм: подождите документ
Когда ифрейм – с того же источника, мы имеем доступ к документу в нём. Но есть подвох. Не связанный с кросс-доменными особенностями, но достаточно важный, чтобы о нём знать.
Когда ифрейм создан, в нём сразу есть документ. Но этот документ – другой, не тот, который в него будет загружен!
Так что если мы тут же сделаем что-то с этим документом, то наши изменения, скорее всего, пропадут.
Вот, взгляните:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// загруженный document - не тот, который был в iframe при создании изначально!
alert(oldDoc == newDoc); // false
};
</script>
Нам не следует работать с документом ещё не загруженного ифрейма, так как это не тот документ. Если мы поставим на него обработчики событий – они будут проигнорированы.
Как поймать момент, когда появится правильный документ?
Правильный документ точно будет доступен, когда сработает iframe.onload
. Но он срабатывает только тогда, когда загрузится весь ифрейм со всеми его ресурсами.
Можно проверять через setInterval
:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
// каждый 100 мс проверяем, не изменился ли документ
let timer = setInterval(() => {
let newDoc = iframe.contentDocument;
if (newDoc == oldDoc) return;
alert("New document is here!");
clearInterval(timer); // отключим setInterval, он нам больше не нужен
}, 100);
</script>
Коллекция window.frames
Другой способ получить объект window
из <iframe>
– забрать его из именованной коллекции window.frames
:
- По номеру:
window.frames[0]
– объектwindow
для первого фрейма в документе. - По имени:
window.frames.iframeName
– объектwindow
для фрейма со свойствомname="iframeName"
.
Например:
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
Ифрейм может иметь другие ифреймы внутри. Таким образом, объекты window
создают иерархию.
Навигация по ним выглядит так:
window.frames
– коллекция «дочерних»window
(для вложенных фреймов).window.parent
– ссылка на «родительский» (внешний)window
.window.top
– ссылка на самого верхнего родителя.
Например:
window.frames[0].parent === window; // true
Можно использовать свойство top
, чтобы проверять, открыт ли текущий документ внутри ифрейма или нет:
if (window == top) { // текущий window == window.top?
alert('Скрипт находится в самом верхнем объекте window, не во фрейме');
} else {
alert('Скрипт запущен во фрейме!');
}
Атрибут ифрейма sandbox
Атрибут sandbox
позволяет наложить ограничения на действия внутри <iframe>
, чтобы предотвратить выполнение ненадёжного кода. Атрибут помещает ифрейм в «песочницу», отмечая его как имеющий другой источник и/или накладывая на него дополнительные ограничения.
Существует список «по умолчанию» ограничений, которые накладываются на <iframe sandbox src="...">
. Их можно уменьшить, если указать в атрибуте список исключений (специальными ключевыми словами), которые не нужно применять, например: <iframe sandbox="allow-forms allow-popups">
.
Другими словами, если у атрибута "sandbox"
нет значения, то браузер применяет максимум ограничений, но через пробел можно указать те из них, которые мы не хотим применять.
Вот список ограничений:
allow-same-origin
"sandbox"
принудительно устанавливает «другой источник» для ифрейма. Другими словами, он заставляет браузер восприниматьiframe
, как пришедший из другого источника, даже еслиsrc
содержит тот же сайт. Со всеми сопутствующими ограничениями для скриптов. Эта опция отключает это ограничение.allow-top-navigation
- Позволяет ифрейму менять
parent.location
. allow-forms
- Позволяет отправлять формы из ифрейма.
allow-scripts
- Позволяет запускать скрипты из ифрейма.
allow-popups
- Позволяет открывать всплывающие окна из ифрейма с помощью
window.open
.
Больше опций можно найти в справочнике.
Пример ниже демонстрирует ифрейм, помещённый в песочницу со стандартным набором ограничений: <iframe sandbox src="...">
. На странице содержится JavaScript и форма.
Обратите внимание, что ничего не работает. Таким образом, набор ограничений по умолчанию очень строгий:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div>Ифрейм ниже имеет атрибут <code>sandbox</code>.</div>
<iframe sandbox src="sandboxed.html" style="height:60px;width:90%"></iframe>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<button onclick="alert(123)">Нажмите для запуска скрипта (не сработает)</button>
<form action="http://google.com">
<input type="text">
<input type="submit" value="Отправить (не сработает)">
</form>
</body>
</html>
Атрибут "sandbox"
создан только для того, чтобы добавлять ограничения. Он не может удалять их. В частности, он не может ослабить ограничения, накладываемые браузером на ифрейм, приходящий с другого источника.
Обмен сообщениями между окнами
Интерфейс postMessage
позволяет окнам общаться между собой независимо от их происхождения.
Это способ обойти политику «Одинакового источника». Он позволяет обмениваться информацией, скажем john-smith.com
и gmail.com
, но только в том случае, если оба сайта согласны и вызывают соответствующие JavaScript-функции. Это делает общение безопасным для пользователя.
Интерфейс имеет две части.
postMessage
Окно, которое хочет отправить сообщение, должно вызвать метод postMessage окна получателя. Другими словами, если мы хотим отправить сообщение в окно win
, тогда нам следует вызвать win.postMessage(data, targetOrigin)
.
Аргументы:
data
- Данные для отправки. Может быть любым объектом, данные клонируются с использованием «алгоритма структурированного клонирования». IE поддерживает только строки, поэтому мы должны использовать метод
JSON.stringify
на сложных объектах, чтобы поддержать этот браузер. targetOrigin
- Определяет источник для окна-получателя, только окно с данного источника имеет право получить сообщение.
Указание targetOrigin
является мерой безопасности. Как мы помним, если окно (получатель) происходит из другого источника, мы из окна-отправителя не можем прочитать его location
. Таким образом, мы не можем быть уверены, какой сайт открыт в заданном окне прямо сейчас: пользователь мог перейти куда-то, окно-отправитель не может это знать.
Если указать targetOrigin
, то мы можем быть уверены, что окно получит данные только в том случае, если в нём правильный сайт. Особенно это важно, если данные конфиденциальные.
Например, здесь win
получит сообщения только в том случае, если в нём открыт документ из источника http://example.com
:
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "http://example.com");
</script>
Если мы не хотим проверять, то в targetOrigin
можно указать *
.
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "*");
</script>
Событие message
Чтобы получать сообщения, окно-получатель должно иметь обработчик события message
(сообщение). Оно срабатывает, когда был вызван метод postMessage
(и проверка targetOrigin
пройдена успешно).
Объект события имеет специфичные свойства:
data
- Данные из
postMessage
. origin
- Источник отправителя, например,
http://javascript.info
. source
- Ссылка на окно-отправитель. Можно сразу отправить что-то в ответ, вызвав
source.postMessage(...)
.
Чтобы добавить обработчик, следует использовать метод addEventListener
, короткий синтаксис window.onmessage
не работает.
Вот пример:
window.addEventListener("message", function(event) {
if (event.origin != 'http://javascript.info') {
// что-то пришло с неизвестного домена. Давайте проигнорируем это
return;
}
alert( "received: " + event.data );
// can message back using event.source.postMessage(...)
});
Полный пример:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Получение ифрейма.
<script>
window.addEventListener('message', function(event) {
alert(`Получено ${event.data} из ${event.origin}`);
});
</script>
</body>
</html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<form id="form">
<input type="text" placeholder="Введите сообщение" name="message">
<input type="submit" value="Нажмите для отправки">
</form>
<iframe src="iframe.html" id="iframe" style="display:block;height:60px"></iframe>
<script>
form.onsubmit = function() {
iframe.contentWindow.postMessage(this.message.value, '*');
return false;
};
</script>
</body>
</html>
Между postMessage
и событием message
не существует задержки. Событие происходит синхронно, быстрее, чем setTimeout(...,0)
.
Итого
Чтобы вызвать метод или получить содержимое из другого окна, нам, во-первых, необходимо иметь ссылку на него.
Для всплывающих окон (попапов) доступны ссылки в обе стороны:
- При открытии окна:
window.open
открывает новое окно и возвращает ссылку на него, - Изнутри открытого окна:
window.opener
– ссылка на открывающее окно.
Для ифреймов мы можем иметь доступ к родителям/потомкам, используя:
window.frames
– коллекция объектовwindow
вложенных ифреймов,window.parent
,window.top
– это ссылки на родительское окно и окно самого верхнего уровня,iframe.contentWindow
– это объектwindow
внутри тега<iframe>
.
Если окна имеют одинаковый источник (протокол, домен, порт), то они могут делать друг с другом всё, что угодно.
В противном случае возможны только следующие действия:
- Изменение свойства location другого окна (доступ только на запись).
- Отправить туда сообщение.
Исключения:
- Окна, которые имеют общий домен второго уровня:
a.site.com
иb.site.com
. Установка свойстваdocument.domain='site.com'
в обоих окнах переведёт их в состояние «Одинакового источника». - Если у ифрейма установлен атрибут
sandbox
, это принудительно переведёт окна в состояние «разных источников», если не установить в атрибут значениеallow-same-origin
. Это можно использовать для запуска ненадёжного кода в ифрейме с того же сайта.
Метод postMessage
позволяет общаться двум окнам с любыми источниками:
-
Отправитель вызывает
targetWin.postMessage(data, targetOrigin)
. -
Если
targetOrigin
не'*'
, тогда браузер проверяет имеет лиtargetWin
источникtargetOrigin
. -
Если это так, тогда
targetWin
вызывает событиеmessage
со специальными свойствами:origin
– источник окна отправителя (например,http://my.site.com
)source
– ссылка на окно отправитель.data
– данные, может быть объектом везде, кроме IE (в IE только строки).
В окне-получателе следует добавить обработчик для этого события с помощью метода
addEventListener
.