Komunikace napříč okny

Zásada „Stejný původ“ (stejný web) omezuje přístup oken a rámů k sobě navzájem.

Myšlenka je taková, že pokud má uživatel otevřené dvě stránky:jedna z john-smith.com a další je gmail.com , pak by nechtěli skript z john-smith.com pro čtení naší pošty od gmail.com . Účelem zásady „Stejný původ“ je tedy chránit uživatele před krádeží informací.

Stejný původ

Dvě adresy URL mají „stejný původ“, pokud mají stejný protokol, doménu a port.

Všechny tyto adresy URL sdílejí stejný původ:

  • http://site.com
  • http://site.com/
  • http://site.com/my/page.html

Tyto ne:

  • http://www.site.com (jiná doména:www. záleží)
  • http://site.org (jiná doména:.org záleží)
  • https://site.com (jiný protokol:https )
  • http://site.com:8080 (jiný port:8080 )

Zásada „Stejný původ“ uvádí, že:

  • pokud máme odkaz na jiné okno, např. vyskakovací okno vytvořené window.open nebo okno uvnitř <iframe> a toto okno pochází ze stejného původu, pak máme k tomuto oknu plný přístup.
  • jinak, pokud pochází z jiného zdroje, pak nebudeme mít přístup k obsahu tohoto okna:proměnné, dokument, nic. Jedinou výjimkou je location :můžeme to změnit (tedy přesměrovat uživatele). Ale nemůžeme číst umístění (takže nevidíme, kde se uživatel nyní nachází, nedochází k úniku informací).

V akci:iframe

<iframe> tag hostí samostatné vložené okno s vlastním samostatným document a window objektů.

Můžeme k nim přistupovat pomocí vlastností:

  • iframe.contentWindow aby se okno dostalo do <iframe> .
  • iframe.contentDocument dostat dokument do <iframe> , zkratka pro iframe.contentWindow.document .

Když přistupujeme k něčemu uvnitř vloženého okna, prohlížeč zkontroluje, zda má iframe stejný původ. Pokud tomu tak není, je přístup odepřen (zápis na location je výjimkou, je stále povoleno).

Zkusme například číst a zapisovat do <iframe> z jiného původu:

<iframe src="https://example.com" id="iframe"></iframe>

<script>
 iframe.onload = function() {
 // we can get the reference to the inner window
 let iframeWindow = iframe.contentWindow; // OK
 try {
 // ...but not to the document inside it
 let doc = iframe.contentDocument; // ERROR
 } catch(e) {
 alert(e); // Security Error (another origin)
 }

 // also we can't READ the URL of the page in iframe
 try {
 // Can't read URL from the Location object
 let href = iframe.contentWindow.location.href; // ERROR
 } catch(e) {
 alert(e); // Security Error
 }

 // ...we can WRITE into location (and thus load something else into the iframe)!
 iframe.contentWindow.location = '/'; // OK

 iframe.onload = null; // clear the handler, not to run it after the location change
 };
</script>

Výše uvedený kód zobrazuje chyby pro všechny operace kromě:

  • Získání odkazu na vnitřní okno iframe.contentWindow – to je povoleno.
  • Zápis na location .

Na rozdíl od toho, pokud <iframe> má stejný původ, můžeme s ním dělat cokoli:

<!-- iframe from the same site -->
<iframe src="/" id="iframe"></iframe>

<script>
 iframe.onload = function() {
 // just do anything
 iframe.contentDocument.body.prepend("Hello, world!");
 };
</script>
iframe.onload vs iframe.contentWindow.onload

iframe.onload událost (na <iframe> tag) je v podstatě stejný jako iframe.contentWindow.onload (na vloženém objektu okna). Spustí se, když se vložené okno plně načte se všemi prostředky.

…Ale nemůžeme získat přístup k iframe.contentWindow.onload pro prvek iframe z jiného původu, tedy pomocí iframe.onload .

Windows na subdoménách:document.domain

Podle definice mají dvě adresy URL s různými doménami různý původ.

Ale pokud Windows sdílejí stejnou doménu druhé úrovně, například john.site.com , peter.site.com a site.com (takže jejich společná doména druhé úrovně je site.com ), můžeme přimět prohlížeč, aby tento rozdíl ignoroval, takže je lze pro účely komunikace napříč okny považovat za pocházející ze „stejného původu“.

Aby to fungovalo, každé takové okno by mělo spustit kód:

document.domain = 'site.com';

To je vše. Nyní mohou komunikovat bez omezení. Opět je to možné pouze pro stránky se stejnou doménou druhé úrovně.

Zastaralé, ale stále funkční

document.domain vlastnost je v procesu odstraňování ze specifikace. Doporučenou náhradou je zasílání zpráv napříč okny (vysvětleno brzy níže).

To znamená, že od nynějška to všechny prohlížeče podporují. A podpora bude zachována pro budoucnost, abychom neporušili starý kód, který se opírá o document.domain .

Iframe:úskalí nesprávného dokumentu

Když prvek iframe pochází ze stejného původu a my můžeme přistupovat k jeho document , je tu úskalí. Nesouvisí s věcmi napříč původy, ale je důležité to vědět.

Po svém vytvoření má iframe okamžitě dokument. Ale tento dokument se liší od toho, který se do něj načítá!

Takže pokud s dokumentem okamžitě něco uděláme, pravděpodobně se to ztratí.

Tady se podívejte:

<iframe src="/" id="iframe"></iframe>

<script>
 let oldDoc = iframe.contentDocument;
 iframe.onload = function() {
 let newDoc = iframe.contentDocument;
 // the loaded document is not the same as initial!
 alert(oldDoc == newDoc); // false
 };
</script>

Neměli bychom pracovat s dokumentem s dosud nenačteným prvkem iframe, protože to je nesprávný dokument . Pokud na něj nastavíme nějaké obslužné rutiny událostí, budou ignorovány.

Jak zjistit okamžik, kdy tam dokument je?

Správný dokument je určitě na místě, když iframe.onload spouštěče. Spustí se ale pouze tehdy, když je načten celý iframe se všemi prostředky.

Můžeme se pokusit zachytit okamžik dříve pomocí kontrol v setInterval :

<iframe src="/" id="iframe"></iframe>

<script>
 let oldDoc = iframe.contentDocument;

 // every 100 ms check if the document is the new one
 let timer = setInterval(() => {
 let newDoc = iframe.contentDocument;
 if (newDoc == oldDoc) return;

 alert("New document is here!");

 clearInterval(timer); // cancel setInterval, don't need it any more
 }, 100);
</script>

Kolekce:window.frames

Alternativní způsob, jak získat objekt okna pro <iframe> – je získat jej z pojmenované kolekce window.frames :

  • Podle čísla:window.frames[0] – objekt okna pro první snímek v dokumentu.
  • Podle jména:window.frames.iframeName – objekt okna pro rám s name="iframeName" .

Například:

<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>

<script>
 alert(iframe.contentWindow == frames[0]); // true
 alert(iframe.contentWindow == frames.win); // true
</script>

Prvek iframe může obsahovat další prvky iframe. Odpovídající window objekty tvoří hierarchii.

Navigační odkazy jsou:

  • window.frames – kolekce „dětských“ oken (pro vnořené rámečky).
  • window.parent – odkaz na „nadřazené“ (vnější) okno.
  • window.top – odkaz na nejvyšší nadřazené okno.

Například:

window.frames[0].parent === window; // true

Můžeme použít top vlastnost pro kontrolu, zda je aktuální dokument otevřen uvnitř rámce nebo ne:

if (window == top) { // current window == window.top?
 alert('The script is in the topmost window, not in a frame');
} else {
 alert('The script runs in a frame!');
}

Atribut iframe „sandbox“

sandbox atribut umožňuje vyloučení určitých akcí uvnitř <iframe> aby se zabránilo spouštění nedůvěryhodného kódu. „Sandboxuje“ prvek iframe tím, že jej považuje za pocházející z jiného původu a/nebo uplatňuje jiná omezení.

Pro <iframe sandbox src="..."> platí „výchozí sada“ omezení . Můžeme se ale uklidnit, když poskytneme mezerami oddělený seznam omezení, která by neměla být použita jako hodnota atributu, jako je tento:<iframe sandbox="allow-forms allow-popups"> .

Jinými slovy, prázdné "sandbox" atribut klade nejpřísnější možná omezení, ale můžeme vložit mezerou oddělený seznam těch, která chceme zrušit.

Zde je seznam omezení:

allow-same-origin
Ve výchozím nastavení "sandbox" vynutí zásadu „jiného původu“ pro prvek iframe. Jinými slovy, přiměje prohlížeč, aby zacházel s iframe jako pocházející z jiného původu, i když jeho src ukazuje na stejný web. Se všemi předpokládanými omezeními pro skripty. Tato možnost tuto funkci odstraní.
allow-top-navigation
Povoluje iframe pro změnu parent.location .
allow-forms
Umožňuje odesílat formuláře z iframe .
allow-scripts
Umožňuje spouštět skripty z iframe .
allow-popups
Umožňuje window.open vyskakovací okna z iframe

Další informace naleznete v příručce.

Níže uvedený příklad ukazuje izolovaný prvek iframe s výchozí sadou omezení:<iframe sandbox src="..."> . Má nějaký JavaScript a formulář.

Upozorňujeme, že nic nefunguje. Takže výchozí nastavení je opravdu drsné:

Resultindex.htmlsandboxed.html
<!doctype html>
<html>

<head>
 <meta charset="UTF-8">
</head>

<body>

 <div>The iframe below has the <code>sandbox</code> attribute.</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)">Click to run a script (doesn't work)</button>

 <form action="http://google.com">
 <input type="text">
 <input type="submit" value="Submit (doesn't work)">
 </form>

</body>
</html>
Poznámka:

Účel "sandbox" atribut slouží pouze k přidání dalších omezení. Nelze je odstranit. Zejména nemůže uvolnit omezení stejného původu, pokud prvek iframe pochází z jiného původu.

Zasílání zpráv napříč okny

postMessage rozhraní umožňuje oknům komunikovat mezi sebou bez ohledu na jejich původ.

Jde tedy o způsob, jak obejít politiku „stejného původu“. Umožňuje okno od john-smith.com mluvit s gmail.com a vyměňovat si informace, ale pouze pokud oba souhlasí a volají odpovídající funkce JavaScriptu. Díky tomu je pro uživatele bezpečný.

Rozhraní má dvě části.

postMessage

Okno, které chce odeslat zprávu, zavolá metodu postMessage přijímacího okna. Jinými slovy, pokud chceme poslat zprávu na win , měli bychom zavolat win.postMessage(data, targetOrigin) .

Argumenty:

data
Data k odeslání. Může to být libovolný objekt, data jsou klonována pomocí „strukturovaného serializačního algoritmu“. IE podporuje pouze řetězce, takže bychom měli JSON.stringify komplexní objekty pro podporu tohoto prohlížeče.
targetOrigin
Určuje zdroj pro cílové okno, takže zprávu obdrží pouze okno z daného zdroje.

targetOrigin je bezpečnostní opatření. Pamatujte, že pokud cílové okno pochází z jiného zdroje, nemůžeme přečíst jeho location v okně odesílatele. Nemůžeme si tedy být jisti, který web je v zamýšleném okně právě otevřen:uživatel by mohl odejít a okno odesílatele o tom nemá ani ponětí.

Zadání targetOrigin zajišťuje, že okno přijímá data pouze v případě, že je stále na správném místě. Důležité, když jsou data citlivá.

Například zde win obdrží zprávu pouze v případě, že má dokument z původu http://example.com :

<iframe src="http://example.com" name="example">

<script>
 let win = window.frames.example;

 win.postMessage("message", "http://example.com");
</script>

Pokud tuto kontrolu nechceme, můžeme nastavit targetOrigin na * .

<iframe src="http://example.com" name="example">

<script>
 let win = window.frames.example;

 win.postMessage("message", "*");
</script>

onmessage

Chcete-li přijmout zprávu, cílové okno by mělo mít handler na message událost. Spustí se, když postMessage se nazývá (a targetOrigin kontrola je úspěšná).

Objekt události má speciální vlastnosti:

data
Data z postMessage .
origin
Původ odesílatele, například http://javascript.info .
source
Odkaz na okno odesílatele. Můžeme okamžitě source.postMessage(...) zpět, pokud chceme.

K přiřazení tohoto handleru bychom měli použít addEventListener , krátká syntaxe window.onmessage nefunguje.

Zde je příklad:

window.addEventListener("message", function(event) {
 if (event.origin != 'http://javascript.info') {
 // something from an unknown domain, let's ignore it
 return;
 }

 alert( "received: " + event.data );

 // can message back using event.source.postMessage(...)
});

Úplný příklad:

Resultiframe.htmlindex.html
<!doctype html>
<html>

<head>
 <meta charset="UTF-8">
</head>

<body>

 Receiving iframe.
 <script>
 window.addEventListener('message', function(event) {
 alert(`Received ${event.data} from ${event.origin}`);
 });
 </script>

</body>
</html>
<!doctype html>
<html>

<head>
 <meta charset="UTF-8">
</head>

<body>

 <form id="form">
 <input type="text" placeholder="Enter message" name="message">
 <input type="submit" value="Click to send">
 </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>

Shrnutí

Abychom mohli volat metody a přistupovat k obsahu jiného okna, měli bychom na něj mít nejprve odkaz.

Pro vyskakovací okna máme tyto odkazy:

  • V okně otevření:window.open – otevře nové okno a vrátí na něj odkaz,
  • Z vyskakovacího okna:window.opener – je odkaz na otevírací okno z vyskakovacího okna.

U prvků iframe můžeme přistupovat k oknům rodičů a dětí pomocí:

  • window.frames – kolekce vnořených okenních objektů,
  • window.parent , window.top jsou odkazy na nadřazená a horní okna,
  • iframe.contentWindow je okno uvnitř <iframe> tag.

Pokud okna sdílejí stejný původ (hostitel, port, protokol), mohou si mezi sebou okna dělat, co chtějí.

V opačném případě jsou možné pouze tyto akce:

  • Změňte location jiného okna (přístup pouze pro zápis).
  • Napište do něj zprávu.

Výjimky jsou:

  • Windows, která sdílejí stejnou doménu druhé úrovně:a.site.com a b.site.com . Poté nastavte document.domain='site.com' v obou je uvádí do stavu „stejného původu“.
  • Pokud má prvek iframe hodnotu sandbox atribut, je násilně uveden do stavu „jiný původ“, pokud allow-same-origin je uvedeno v hodnotě atributu. To lze použít ke spouštění nedůvěryhodného kódu v prvcích iframe ze stejného webu.

postMessage rozhraní umožňuje mluvit dvěma oknům s libovolným původem:

  1. Odesílatel volá targetWin.postMessage(data, targetOrigin) .

  2. Pokud targetOrigin není '*' , pak prohlížeč zkontroluje, zda okno targetWin má původ targetOrigin .

  3. Pokud ano, pak targetWin spustí message událost se speciálními vlastnostmi:

    • origin – původ okna odesílatele (např. http://my.site.com )
    • source – odkaz na okno odesílatele.
    • data – data, jakýkoli objekt všude kromě IE, který podporuje pouze řetězce.

    Měli bychom použít addEventListener pro nastavení obsluhy pro tuto událost v cílovém okně.