2022年4月13日

ウィンドウ間の通信

「同一オリジン」(同一サイト)ポリシーは、ウィンドウとフレームがお互いにアクセスすることを制限します。

その考え方は、ユーザーが2つのページを開いている場合、1つはjohn-smith.com、もう1つはgmail.comの場合、john-smith.comのスクリプトがgmail.comからメールを読み取ることは望ましくないということです。したがって、「同一オリジン」ポリシーの目的は、ユーザーを情報窃盗から保護することです。

同一オリジン

2つの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です。変更することができます(これによりユーザーをリダイレクトします)。しかし、locationを読み取ることはできません(そのため、ユーザーがどこにいるかを確認することはできません。情報漏えいはありません)。

動作: iframe

<iframe>タグは、独自の分離されたdocumentおよびwindowオブジェクトを持つ、別の埋め込みウィンドウをホストします。

プロパティを使用してそれらにアクセスできます

  • <iframe>内のウィンドウを取得するにはiframe.contentWindowを使用します。
  • <iframe>内のドキュメントを取得するにはiframe.contentDocumentを使用します。これはiframe.contentWindow.documentの省略形です。

埋め込みウィンドウ内のものにアクセスするとき、ブラウザはiframeが同じオリジンを持っているかどうかを確認します。そうでない場合、アクセスは拒否されます(locationへの書き込みは例外で、引き続き許可されます)。

たとえば、別のオリジンから<iframe>の読み取りと書き込みを試してみましょう

<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>

上記のコードは、以下を除くすべての操作でエラーを示しています

  • 内部ウィンドウへの参照を取得するiframe.contentWindow - これは許可されています。
  • locationへの書き込み。

それとは対照的に、<iframe>が同じオリジンを持っている場合、それに対して何でもできます

<!-- 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イベント(<iframe>タグ上)は、基本的にはiframe.contentWindow.onload(埋め込みウィンドウオブジェクト上)と同じです。埋め込みウィンドウがすべてのリソースとともに完全にロードされるとトリガーされます。

…しかし、別のオリジンからのiframeの場合、iframe.contentWindow.onloadにアクセスできないため、iframe.onloadを使用します。

サブドメイン上のウィンドウ: document.domain

定義上、異なるドメインを持つ2つのURLは異なるオリジンを持ちます。

しかし、ウィンドウが同じ第2レベルドメインを共有している場合、たとえばjohn.site.competer.site.comsite.com(共通の第2レベルドメインがsite.comになるように)、ブラウザにその違いを無視させることができます。したがって、ウィンドウ間の通信の目的のために、「同じオリジン」から来たものとして扱われるようにできます。

機能させるには、そのような各ウィンドウがコードを実行する必要があります

document.domain = 'site.com';

それだけです。これで、制限なしに相互作用できます。繰り返しますが、これは同じ第2レベルドメインを持つページでのみ可能です。

非推奨ですが、まだ動作しています

document.domainプロパティは、仕様から削除される過程にあります。クロスウィンドウメッセージング(以下で説明)が推奨される代替手段です。

とはいえ、現在、すべてのブラウザがそれをサポートしています。また、document.domainに依存する古いコードを壊さないように、将来もサポートは維持されます。

Iframe: 間違ったドキュメントの落とし穴

iframeが同じオリジンから来て、そのdocumentにアクセスできる場合、落とし穴があります。クロスオリジンのこととは関係ありませんが、知っておくことが重要です。

作成時、iframeにはすぐにドキュメントがあります。しかし、そのドキュメントは、ロードされるものとは異なります!

そのため、すぐにドキュメントで何かを行うと、おそらく失われます。

ここに見てください

<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>

まだロードされていないiframeのドキュメントを操作するべきではありません。それは間違ったドキュメントだからです。そこにイベントハンドラーを設定しても、無視されます。

ドキュメントが存在する瞬間を検出する方法は?

iframe.onloadがトリガーされたときに、正しいドキュメントが確実に配置されます。ただし、すべてのリソースを含むiframe全体がロードされた場合にのみトリガーされます。

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>

コレクション: window.frames

<iframe>のwindowオブジェクトを取得する別の方法は、名前付きコレクションwindow.framesから取得することです

  • 数値で: window.frames[0] - ドキュメント内の最初のフレームのwindowオブジェクト。
  • 名前で: window.frames.iframeName - name="iframeName"を持つフレームのwindowオブジェクト。

たとえば

<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('The script is in the topmost window, not in a frame');
} else {
  alert('The script runs in a frame!');
}

「サンドボックス」iframe属性

sandbox属性を使用すると、信頼できないコードの実行を防ぐために、<iframe>内での特定のアクションを除外できます。別のオリジンから来たものとして、または他の制限を適用することにより、iframeを「サンドボックス化」します。

<iframe sandbox src="...">に適用される制限の「デフォルトセット」があります。ただし、<iframe sandbox="allow-forms allow-popups">のように、属性の値として適用されない制限のスペース区切りのリストを提供すると、緩和できます。

言い換えれば、空の"sandbox"属性は可能な限り最も厳しい制限を適用しますが、リフトしたいもののスペース区切りリストを配置できます。

制限のリストを次に示します

allow-same-origin
デフォルトでは、"sandbox"はiframeに対して「異なるオリジン」ポリシーを強制します。言い換えれば、srcが同じサイトを指している場合でも、ブラウザにiframeを別のオリジンから来たものとして扱わせます。スクリプトに対するすべての暗黙の制限付きです。このオプションはその機能を削除します。
allow-top-navigation
iframeparent.locationを変更できるようにします。
allow-forms
iframeからフォームを送信できるようにします。
allow-scripts
iframeからスクリプトを実行できるようにします。
allow-popups
iframeからwindow.openポップアップを開くことができるようにします

詳細については、マニュアルを参照してください。

以下の例は、デフォルトの制限セットを持つサンドボックス化されたiframeを示しています: <iframe sandbox src="...">。いくつかのJavaScriptとフォームがあります。

何も機能しないことに注意してください。したがって、デフォルトセットは非常に厳しいものです

結果
index.html
sandboxed.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>
注意してください

"sandbox"属性の目的は、制限を追加するだけです。削除することはできません。特に、iframeが別のオリジンから来た場合、同一オリジンの制限を緩和することはできません。

ウィンドウ間のメッセージング

postMessageインターフェースを使用すると、ウィンドウはどのオリジンから来たかに関係なく、お互いに通信できます。

したがって、「同一オリジン」ポリシーを回避する方法です。john-smith.comからのウィンドウがgmail.comと通信し、情報を交換することを許可しますが、両方が同意し、対応するJavaScript関数を呼び出す場合に限ります。これにより、ユーザーにとって安全になります。

インターフェースには2つの部分があります。

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>

onmessage

メッセージを受信するには、ターゲットウィンドウは message イベントのハンドラを持っている必要があります。これは postMessage が呼び出された時(および targetOrigin チェックが成功した時)にトリガーされます。

イベントオブジェクトには特別なプロパティがあります。

data
postMessage からのデータ。
origin
送信者のオリジン。例えば https://javascriptinfo.dokyumento.jp のようなものです。
source
送信者ウィンドウへの参照。必要であれば、すぐに source.postMessage(...) で折り返すことができます。

そのハンドラを割り当てるには、addEventListener を使う必要があります。短縮構文の window.onmessage は機能しません。

以下に例を示します。

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

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

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

完全な例

結果
iframe.html
index.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>

まとめ

別のウィンドウのメソッドを呼び出してコンテンツにアクセスするには、まずそのウィンドウへの参照が必要です。

ポップアップの場合、次の参照があります。

  • オープナーウィンドウから: window.open – 新しいウィンドウを開き、それへの参照を返します。
  • ポップアップから: window.opener – ポップアップからのオープナーウィンドウへの参照です。

iframe の場合、親/子ウィンドウには次を使用してアクセスできます。

  • window.frames – ネストされたウィンドウオブジェクトのコレクション。
  • window.parentwindow.top は、親および最上位のウィンドウへの参照です。
  • iframe.contentWindow は、<iframe> タグ内のウィンドウです。

ウィンドウが同じオリジン(ホスト、ポート、プロトコル)を共有している場合、ウィンドウは互いに何でもやりたいことができます。

そうでない場合、可能なアクションは次のとおりです。

  • 別のウィンドウの location を変更する(書き込み専用アクセス)。
  • それにメッセージを投稿します。

例外は次のとおりです。

  • 同じセカンドレベルドメインを共有するウィンドウ: a.site.com および b.site.com。その場合、両方で document.domain='site.com' を設定すると、両方が「同じオリジン」状態になります。
  • iframe に sandbox 属性がある場合、属性値に allow-same-origin が指定されていない限り、強制的に「異なるオリジン」状態になります。これは、同じサイトからの iframe で信頼できないコードを実行するために使用できます。

postMessage インターフェースを使用すると、任意のオリジンを持つ 2 つのウィンドウが通信できます。

  1. 送信者は targetWin.postMessage(data, targetOrigin) を呼び出します。

  2. targetOrigin'*' でない場合、ブラウザはウィンドウ targetWin がオリジン targetOrigin を持っているかどうかをチェックします。

  3. その場合、targetWin は、特別なプロパティを持つ message イベントをトリガーします。

    • origin – 送信元ウィンドウのオリジン(http://my.site.com など)
    • source – 送信元ウィンドウへの参照。
    • data – データ。IE を除くすべての場所の任意のオブジェクト(IE は文字列のみをサポート)。

    ターゲットウィンドウ内でこのイベントのハンドラを設定するには、addEventListener を使用する必要があります。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点に関する提案がある場合は、コメントではなく、GitHub issue を送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを数語挿入する場合は、<code> タグを使用し、複数行の場合は <pre> タグでラップし、10行を超える場合はサンドボックス ( plnkr, jsbin, codepen...) を使用してください。