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もあるため、送信元と要求への対応を確認してデータを処理します。
操作履歴と解説を振り返れます。
ブラウザ / オリジン・通信記録
(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": "子文書"
}(sameFrame.contentDocument.querySelector("h1").textContent="親から変更",sameFrame.contentDocument.querySelector("h1").textContent)"親から変更"
(globalThis.crossFrame=await openFrame("http://127.0.0.1:4323/child"),{contentDocument:crossFrame.contentDocument,hasWindow:crossFrame.contentWindow!==null}){
"contentDocument": null,
"hasWindow": true
}(()=>{try{return crossFrame.contentWindow.document.title}catch(e){return{error:e.name}}})(){
"error": "SecurityError"
}await exchange(crossFrame,"hello","http://127.0.0.1:4323"){
"origin": "http://127.0.0.1:4323",
"sourceMatches": true,
"data": {
"ack": "hello",
"received": [
"hello"
]
}
}(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"
]
}
}(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
}
](await exchange(sameFrame,"same-child","http://127.0.0.1:4322"),receipts.at(-1)){
"ack": "same-child",
"originOK": false,
"sourceOK": false,
"accepted": false
}(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
}(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
}演習完了