一覧WB-12-3 · オリジン境界とCORS

iframeの制限とpostMessage

同じオリジンと別オリジンの子文書を開き、DOM参照の差を確認します。続いて宛先オリジンを指定してメッセージを送り、受信時のorigin・sourceを検査します。子文書は受け取ったメッセージを順に記録し、親へ返信するように用意されています。

説明

iframeで別文書を表示できることと、その文書のDOMを読めることは別です。同じオリジンなら参照できる内容も、別オリジンでは制限されます。postMessageは、この境界を越えて必要なデータをメッセージとして渡すためのAPIです。

同じオリジンと別オリジンの子文書を開き、DOM参照の差を確認します。続いて宛先オリジンを指定してメッセージを送り、受信時のorigin・sourceを検査します。子文書は受け取ったメッセージを順に記録し、親へ返信するように用意されています。

1同じオリジンの文書を読む

同じ4322番の子文書をiframeで開きます。

(globalThis.sameFrame=await openFrame子文書を開き完了を待つ("http://127.0.0.1:4322/child"),{parent:location.origin,child:sameFrame.contentWindow.location.origin,title:sameFrame.contentDocument子のDocumentを取得.querySelector("h1")子の見出しを参照.textContent})

親子のオリジンが同じなので、子のh1を参照できます。openFrameはiframeを追加して読込み完了を待つ関数です。

2同じオリジンならDOMも更新できる

子文書の見出しを書き換えます。

(sameFrame.contentDocument.querySelector("h1").textContent="親から変更"子のDOMを更新,sameFrame.contentDocument.querySelector("h1").textContent)

親からの変更が子文書へ反映されます。同じオリジンの文書同士は強い参照権限を共有するため、独立して表示しているだけで分離されるわけではありません。

3別オリジンのiframeを開く

4323番の子文書を開き、contentDocumentを確認します。

(globalThis.crossFrame=await openFrame("http://127.0.0.1:4323/child")別オリジンの子を表示,{contentDocument:crossFrame.contentDocumentDocumentの公開状態,hasWindow:crossFrame.contentWindowWindowの参照!==null})

Windowの参照は得られますが、contentDocumentはnullです。画面への埋込みとDOMの公開は別なので、表示できても自由に内容を読めるとは限りません。

4Window経由のDOM参照も拒否される

contentWindow.documentへアクセスします。

(()=>{try{return crossFrame.contentWindow.document別文書のDOMへ参照を試みる.title}catch(e){return{error:e.name参照制限の例外}}})()

SecurityErrorになります。contentWindowを取得できても、その先のプロパティへすべてアクセスできるわけではありません。CORSでFetchの応答を公開しても、別文書のDOM参照が許可されるわけではありません。

5オリジンを指定してメッセージを送る

postMessageで文字列helloを送り、子からの返信を待ちます。

await exchange(crossFrame返信を待ちながら対象Windowへ送る,"hello"送るデータ,"http://127.0.0.1:4323"宛先オリジンを限定)

返信のoriginは4323番で、sourceは送った先のWindowです。exchangeはmessageを待つ処理を登録してからcontentWindow.postMessage(data,targetOrigin)を呼びます。DOMの参照権限を渡さず、決めたデータだけをやり取りできます。

6違う宛先オリジンなら届かない

wrongを4322番宛てとしてcrossFrameへ送り、その後に正しい宛先でbarrierを送ります。

(crossFrame.contentWindow.postMessage("wrong","http://127.0.0.1:4322")一致しない宛先を指定,await exchange(crossFrame,"barrier"後続の返信まで待つ,"http://127.0.0.1:4323"))

子の受信一覧にはhelloとbarrierが入り、wrongはありません。postMessageの対象Windowだけでなく、その文書のオリジンがtargetOriginと一致する必要があります。barrierの返信まで待つことで、先の送信が処理される順序も確認しています。

7受信時にoriginとsourceを検査する

受信ログ用listenerを登録し、期待する子へメッセージを送ります。

(globalThis.receipts=[],globalThis.checkMessage=e=>{const originOK=e.origin===送信元オリジンの検査"http://127.0.0.1:4323",sourceOK=e.source===crossFrame.contentWindow期待するWindowの検査;receipts.push({ack:e.data.ack,originOK,sourceOK,accepted:originOK&&sourceOK})},addEventListener("message",checkMessage)受信処理を登録,await exchange(crossFrame,"checked","http://127.0.0.1:4323"),receipts)

originとsourceの両方が一致し、acceptedはtrueです。originはどのオリジンから来たか、sourceはどのWindowから来たかを表します。データを処理する前に送信元を確かめ、さらにデータの型や項目も検査します。

8別のオリジンからの返信

同じ4322番の子へメッセージを送り、検査結果を確認します。

(await exchange(sameFrame別の子へ送信,"same-child","http://127.0.0.1:4322"),receipts.at(-1)その返信の検査結果)

親と同じオリジンからの返信でも、今の処理が期待する4323番の子ではありません。originOKとsourceOKがfalseとなり、採用されません。「同じオリジンだから常にこの処理の正しい相手」とは限りません。

9同じオリジンでも別Windowを拒否する

4323番の子をもう一つ開き、そこから返信させます。

(globalThis.otherFrame=await openFrame同じオリジンの別Windowを作成("http://127.0.0.1:4323/child"),await exchange(otherFrame新しいWindowから返信,"other-window","http://127.0.0.1:4323"),receipts.at(-1)sourceの不一致を確認)

originOKはtrueですがsourceOKはfalseです。オリジンが同じ二つのWindowを、sourceで区別できます。一つのiframeとの応答を待つ処理なら、そのWindowへ対応付けて判定します。

10不要な受信処理を解除する

検査用listenerを解除し、もう一度正しい相手へ送ります。

(globalThis.count=receipts.length,removeEventListener("message",checkMessage)登録した関数で解除,await exchange(crossFrame,"after-remove","http://127.0.0.1:4323"),{before:count,after:receipts.length解除後の記録数})

返信は届きますが、解除したlistenerのログ件数は増えません。文書間通信でも、画面の終了や相手の入替えに合わせて不要なlistenerを解除します。

まとめ

iframeの表示、Window参照、DOM参照はそれぞれ許可範囲が異なります。

postMessageでは宛先オリジンを指定し、受信時にはoriginとsourceを検査します。

同じオリジンの別Windowもあるため、送信元と要求への対応を確認してデータを処理します。

操作履歴と解説を振り返れます。

ブラウザ / オリジン・通信記録

1
(globalThis.sameFrame=await openFrame("http://127.0.0.1:4322/child"),{parent:location.origin,child:sameFrame.contentWindow.location.origin,title:sameFrame.contentDocument.querySelector("h1").textContent})
{
  "parent": "http://127.0.0.1:4322",
  "child": "http://127.0.0.1:4322",
  "title": "子文書"
}
2
(sameFrame.contentDocument.querySelector("h1").textContent="親から変更",sameFrame.contentDocument.querySelector("h1").textContent)
"親から変更"
3
(globalThis.crossFrame=await openFrame("http://127.0.0.1:4323/child"),{contentDocument:crossFrame.contentDocument,hasWindow:crossFrame.contentWindow!==null})
{
  "contentDocument": null,
  "hasWindow": true
}
4
(()=>{try{return crossFrame.contentWindow.document.title}catch(e){return{error:e.name}}})()
{
  "error": "SecurityError"
}
5
await exchange(crossFrame,"hello","http://127.0.0.1:4323")
{
  "origin": "http://127.0.0.1:4323",
  "sourceMatches": true,
  "data": {
    "ack": "hello",
    "received": [
      "hello"
    ]
  }
}
6
(crossFrame.contentWindow.postMessage("wrong","http://127.0.0.1:4322"),await exchange(crossFrame,"barrier","http://127.0.0.1:4323"))
{
  "origin": "http://127.0.0.1:4323",
  "sourceMatches": true,
  "data": {
    "ack": "barrier",
    "received": [
      "hello",
      "barrier"
    ]
  }
}
7
(globalThis.receipts=[],globalThis.checkMessage=e=>{const originOK=e.origin==="http://127.0.0.1:4323",sourceOK=e.source===crossFrame.contentWindow;receipts.push({ack:e.data.ack,originOK,sourceOK,accepted:originOK&&sourceOK})},addEventListener("message",checkMessage),await exchange(crossFrame,"checked","http://127.0.0.1:4323"),receipts)
[
  {
    "ack": "checked",
    "originOK": true,
    "sourceOK": true,
    "accepted": true
  }
]
8
(await exchange(sameFrame,"same-child","http://127.0.0.1:4322"),receipts.at(-1))
{
  "ack": "same-child",
  "originOK": false,
  "sourceOK": false,
  "accepted": false
}
9
(globalThis.otherFrame=await openFrame("http://127.0.0.1:4323/child"),await exchange(otherFrame,"other-window","http://127.0.0.1:4323"),receipts.at(-1))
{
  "ack": "other-window",
  "originOK": true,
  "sourceOK": false,
  "accepted": false
}
10
(globalThis.count=receipts.length,removeEventListener("message",checkMessage),await exchange(crossFrame,"after-remove","http://127.0.0.1:4323"),{before:count,after:receipts.length})
{
  "before": 3,
  "after": 3
}

演習完了