Політика “Одного походження” (Same Origin), іншими словами один й той самий сайт, вона обмежує доступ вікон і фреймів один до одного.
Ідея полягає в тому, що якщо користувач має дві відкриті сторінки: одну з john-smith.com
, а іншу – gmail.com
, тоді він не хоче, щоб скрипт з john-smith.com
читав пошту з gmail.com
. Отже, мета політики “Одного походження” – захистити користувачів від крадіжки даних.
Політика "Одного походження" (Same Origin)
Кажуть, що дві 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.open
або вікно всередині<iframe>
, і воно має те саме походження, то ми маємо повний доступ до нього. - в іншому випадку, якщо походження відрізняється, ми не можемо отримати доступ до вмісту цього вікна: змінних, документа, будь-чого. Єдиним винятком є
location
: ми можемо змінити його (таким чином перенаправити користувача). Але ми не можемо читати це місцезнаходження (тому ми не можемо побачити, де зараз перебуває користувач, тому немає витоку інформації).
iframe на практиці
Кожен <iframe>
містить окреме вбудоване вікно з окремими об’єктами document
та window
.
Ми можемо отримати до них доступ за допомогою властивостей:
iframe.contentWindow
, щоб отримати вікно всередині<iframe>
.iframe.contentDocument
, щоб отримати документ всередині<iframe>
, скорочення відiframe.contentWindow.document
.
Коли ми отримуємо доступ до чогось у вбудованому вікні, браузер перевіряє, чи має iframe те саме походження. Якщо це не так, то доступ забороняється (зміни location
є винятком, це дозволено).
Наприклад, давайте спробуємо прочитати та записати щось в <iframe>
з іншим походженням:
<iframe src="https://example.com" id="iframe"></iframe>
<script>
iframe.onload = function() {
// ми можемо отримати посилання на внутрішнє вікно
let iframeWindow = iframe.contentWindow; // OK
try {
// ...але не до документа всередині нього
let doc = iframe.contentDocument; // ПОМИЛКА
} catch(e) {
alert(e); // Security Error (інше походження)
}
// ми також не можемо ПРОЧИТАТИ URL-адресу сторінки в iframe
try {
// Не вдається прочитати URL-адресу з об’єкта Location
let href = iframe.contentWindow.location.href; // ПОМИЛКА
} catch(e) {
alert(e); // Security Error
}
// ...ми можемо ЗМІНИТИ location (і таким чином завантажувати щось інше в iframe)!
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // очищаємо обробник, щоб не запускати його після зміни location
};
</script>
Код вище показує помилки для будь-яких операцій, крім:
- Отримання посилання на внутрішнє вікно
iframe.contentWindow
– це дозволено. - Зміни
location
.
На противагу цьому, якщо <iframe>
має те саме походження, ми можемо робити з ним що завгодно:
<!-- iframe з того ж сайту -->
<iframe src="/" id="iframe"></iframe>
<script>
iframe.onload = function() {
// робіть будь-що
iframe.contentDocument.body.prepend("Привіт, світ!");
};
</script>
iframe.onload
vs iframe.contentWindow.onload
Подія iframe.onload
(у тегу <iframe>
) по суті така ж, як iframe.contentWindow.onload
(у вбудованому об’єкті вікна). Вона запускається, коли вбудоване вікно повністю завантажується зі всіма ресурсами.
…Але ми не можемо отримати доступ до iframe.contentWindow.onload
для iframe з іншим походженням, тому використовуємо iframe.onload
.
Вікна на субдоменах: document.domain
За визначенням, дві URL-адреси з різними доменами мають різне походження.
Але якщо вікна спільно використовують один домен другого рівня, наприклад, john.site.com
, peter.site.com
і site.com
(тобто їхнім спільним доменом другого рівня є site.com
), ми можемо змусити браузер ігнорувати цю різницю, і сприймати їх як сайти “одного походження”, це значно полегшує комунікацію між вікнами.
Щоб це спрацювало, кожне таке вікно має запустити код:
document.domain = 'site.com';
Це все. Тепер вони можуть взаємодіяти без обмежень. Знову ж таки, це можливо лише для сторінок з однаковим доменом другого рівня.
Властивість document.domain
зараз у процесі вилучення зі специфікації. Обмін повідомленнями між вікнами (пояснення невдовзі нижче) є запропонованою заміною.
Тим не менш, наразі всі браузери підтримують її. І підтримку буде збережено на майбутнє, щоб не зламати старий код, який покладається на document.domain
.
Iframe: підводний камінь при роботі з document
Коли iframe має однакове подходження походження з оригінальним сайтом, і ми можемо отримати доступ до його document
, з’являється підводний камінь, про який важливо знати.
Після створення iframe одразу має document. Але цей document буде іншим після того, як закінчеться завантаження iframe!
Тому, якщо ми негайно зробимо щось із документом, зміни, ймовірно, буде втрачено.
Ось подивіться:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// завантажений документ не збігається з початковим!
alert(oldDoc == newDoc); // false
};
</script>
Нам не слід працювати з document ще не завантаженого iframe, тому що це неправильний document. Якщо ми додамо до нього обробники подій, вони будуть проігноровані.
Як визначити момент, коли з document вже можно працювати?
Правильний document вже точно знаходиться на місці, коли запускається iframe.onload
. Але він запускається лише тоді, коли завантажується весь iframe з усіма ресурсами.
Ми можемо спробувати визначити цей момент раніше за допомогою перевірок у setInterval
:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
// кожні 100 мс перевіряємо, чи є document новим
let timer = setInterval(() => {
let newDoc = iframe.contentDocument;
if (newDoc == oldDoc) return;
alert("Новий документ тут!");
clearInterval(timer); // скасуємо setInterval, він більше не потрібен
}, 100);
</script>
Колекція: window.frames
Альтернативний спосіб отримати об’єкт вікна для <iframe>
– це отримати його з іменованої колекції window.frames
:
- За номером:
window.frames[0]
– об’єкт вікна для першого фрейму в документі. - За назвою:
window.frames.iframeName
– об’єкт вікна для фрейму з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>
Усередині iframe можуть бути інші iframe. Відповідні об’єкти window
утворюють ієрархію.
Навігаційні посилання:
window.frames
– колекція дочірніх вікон (для вкладених фреймів).window.parent
– посилання на “батьківське” (зовнішнє) вікно.window.top
– посилання на найвище батьківське вікно.
Наприклад:
window.frames[0].parent === window; // true
Ми можемо використовувати властивість top
, щоб перевірити, чи відкритий поточний документ у фреймі чи ні:
if (window == top) { // current window == window.top?
alert('Скрипт знаходиться у батьківському вікні, а не у фреймі');
} else {
alert('Скрипт виконується у фреймі!');
}
Атрибут iframe “sandbox”
Атрибут sandbox
дозволяє заборонити певні дії всередині <iframe>
, щоб запобігти виконанню коду, якому ми не до кінця довіряємо. Атрибут закриває iframe у “пісочниці”, розглядаючи його як iframe іншого походження та/або застосовуючи інші обмеження.
До <iframe sandbox src="...">
з атрибутом sandbox
застосовується “типовий набір” певних обмежень. Але їх можна послабити, для цього потрібно окремо задати список обмежень, які не слід застосовувати. Назви ціх обмежень потрібно розділити пробілами і записати як значення атрибута sandbox
, наприклад: <iframe sandbox="allow-forms allow-popups">
.
Іншими словами, порожній атрибут "sandbox"
накладає найсуворіші обмеження, але ми можемо помістити розділений пробілами список тих, які ми хочемо зняти.
Ось список обмежень:
allow-same-origin
- Типово атрибут
"sandbox"
нав’язує політику “іншого походження” для iframe. Це змушує браузер сприйматиiframe
, як iframe з іншим походженням, навіть якщо йогоsrc
вказує на той самий сайт. Це застосовує усі неявні обмеженнями для скриптів. Цей параметр вимикає цю функцію. allow-top-navigation
- Дозволяє
iframe
змінитиparent.location
. allow-forms
- Дозволяє надсилати форми з
iframe
. allow-scripts
- Дозволяє запускати скрипти з
iframe
. allow-popups
- Дозволяє
window.open
спливаючі вікна зiframe
Дивіться посібник для отримання додаткової інформації.
Наведений нижче приклад демонструє iframe із ізольованим середовищем із набором обмежень за замовчуванням: <iframe sandbox src="...">
. Він має певний JavaScript і форму.
Зверніть увагу, що нічого не працює. Отже, типовий набір обмежень дійсно суворий:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<div>Наведений нижче iframe має атрибут <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"
– лише додати більше обмежень. Він не може їх видалити. Зокрема, він не може послабити стандартні обмеження щодо походження, якщо iframe насправді має інше походження.
Обмін повідомленнями між вікнами
Інтерфейс postMessage
дозволяє вікнам спілкуватися один з одним незалежно від того, яке в них походження.
Отже, це спосіб обійти політику “Одного походження”. Це дозволяє вікну з john-smith.com
спілкуватися з gmail.com
та обмінюватися інформацією, але лише якщо вони обидва згодні та викликають відповідні функції JavaScript. Це робить його безпечним для користувачів.
Інтерфейс складається з двох частин.
postMessage
Вікно, яке хоче надіслати повідомлення, викликає метод postMessage вікна отримання. Іншими словами, якщо ми хочемо надіслати повідомлення до win
, ми повинні викликати win.postMessage(data, targetOrigin)
.
Аргументи:
data
- Дані для відправки. Можуть бути будь-яким об’єктом, дані клонуються за допомогою “алгоритму структурованої серіалізації”. IE підтримує лише рядки, тому ми повинні застосувати
JSON.stringify
для складних об’єктів для підтримки цього браузера. targetOrigin
- Вказує джерело для цільового вікна, щоб повідомлення отримувало лише вікно з даного джерела.
targetOrigin
є заходом безпеки. Пам’ятайте, якщо цільове вікно походить з іншого джерела, ми не можемо прочитати його location
у вікні відправника. Тому ми не можемо бути впевнені, який сайт зараз відкритий у передбачуваному вікні: користувач міг би піти, а вікно відправника не має про це поняття.
targetOrigin
гарантує, що iframe отримає дані, лише якщо він все ще знаходиться на потрібному сайті. Це важливо, коли дані є чутливими або конфіденційними.
Наприклад, тут win
отримає повідомлення, лише якщо в ньому document з адресою http://example.com
:
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("повідомлення", "http://example.com");
</script>
Якщо ми не хочемо цієї перевірки, ми можемо встановити для targetOrigin
значення *
.
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("повідомлення", "*");
</script>
onmessage
Щоб отримати повідомлення, вікно отримувач має мати обробник події 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( "отримано: " + event.data );
// можна надіслати повідомлення назад за допомогою event.source.postMessage(...)
});
Повний приклад:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
Отримання iframe.
<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>
Підсумки
Щоб викликати методи та отримати доступ до вмісту іншого вікна, ми повинні спочатку мати посилання на нього.
Для спливаючих вікон у нас є такі посилання:
- З вікна відкриття:
window.open
– відкриває нове вікно та повертає посилання на нього, - Зі спливаючого вікна:
window.opener
– це посилання на основне вікно зі спливаючого вікна.
Для iframes ми можемо отримати доступ до батьківських/дочірніх вікон за допомогою:
window.frames
– набір вкладених об’єктів вікна,window.parent
,window.top
посилання на батьківське та верхнє вікна,iframe.contentWindow
– вікно всередині тегу<iframe>
.
Якщо вікна мають однакове походження (хост, порт, протокол), то вони можуть робити між собою все, що захочуть.
В іншому випадку можливі лише такі дії:
- Змінити
location
іншого вікна (доступ лише для запису). - Надіслати на нього повідомлення.
Винятки:
- Вікна, які використовують той самий домен другого рівня:
a.site.com
таb.site.com
. Налаштуванняdocument.domain='site.com'
в обох переведе їх у стан “одного походження”. - Якщо iframe має атрибут
sandbox
, він примусово переводиться в стан “іншого походження”, якщо у значенні атрибута не вказаноallow-same-origin
. Це можна використовувати для запуску коду, якому ми не до кінця довіряємо, в iframes з того самого сайту.
Інтерфейс postMessage
дозволяє надсилати повідомлення двом вікнам з будь-яким походженням:
-
Відправник викликає
targetWin.postMessage(data, targetOrigin)
. -
Якщо значення у
targetOrigin
не'*'
, тоді браузер перевіряє, чи має вікноtargetWin
джерелоtargetOrigin
. -
Якщо це так, то
targetWin
ініціює подіюmessage
зі спеціальними властивостями:origin
– походження вікна відправника (наприклад,http://my.site.com
)source
– посилання на вікно відправника.data
– дані, можуть бути об’єктом скрізь, крім IE (в IE тільки рядок).
Ми повинні використовувати
addEventListener
, щоб встановити обробник для цієї події всередині вікна отримувача.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)