同じ/異なるオリジンのiframeの中からできること

(2018-10-24)

同じ/異なるオリジンのiframeの中からできることを確認する。同じオリジンというのは ホストだけではなくプロトコルやポート番号も同じということ。 Web広告では非同期のレンダリングや、CSSやJSのグローバルスコープといったページ全体に及ぶ影響の分離のためによく使われている。

検証用ページ

3つのiframeがあるページを作った。 それぞれabout:blankで動的に書き込むのと、同じオリジンのhtmlを参照しているものと、異なるオリジンのhtmlを参照しているもの。

$ cat index.html
<html>
<head>
<style type="text/css">
p{
  width:100px;
  height:100px;
  background:#999;
}
</style>
</head>
<body>
  <p>parent</p>
  <div><iframe src="about:blank" id="if1"></iframe></div>
  <script type="text/javascript">
    var parentValue = "PARENT";
    window.addEventListener("message", (event) => {
      console.log(`message from ${event.origin}: ${event.data}`);
    }, false);
  </script>

  <div><iframe src="./iframe.html"></iframe></div>
  <div><iframe src="https://*****.ngrok.io/iframe.html"></iframe></div>
  <script type="text/javascript" src="./index.js"></script>
</body>
</html>

1つ目のiframeの中にscriptタグなどを書き込むJS。

$ cat index.js
const el = document.getElementById("if1");
const doc = el.contentDocument;

const p = doc.createElement('p');
p.textContent = "child";
doc.body.appendChild(p);

var scriptTag = doc.createElement('script');
const script = `
    var childValue = "CHILD";
    console.log(location.href + " parentValue: " + window.parent.parentValue);
    window.frameElement.style.width = '400px';
`;
scriptTag.innerHTML = script;
doc.body.appendChild(scriptTag);
console.log(`${location.href} childValue: ${el.contentWindow.childValue}`);

iframeのsrcで参照するhtml。

$ cat iframe.html
<html>
<head>
</head>
<body>
  <p>child</p> 
  <script type="text/javascript">
    try{
      console.log(`${location.href} parentValue: ${window.parent.parentValue}`);
    } catch (e) {
      console.log(`${location.href} ${e}`);
    }
    window.parent.postMessage("aaaa", "*");
    if (window.frameElement) {
      window.frameElement.style.width = '400px';
    } else {
      console.log(`${location.href} window.frameElement is ${window.frameElement}`);
    }
  </script>
</body>
</html>

実行

異なるホストにするためngrokを立ち上げる。

$ npm install http-server -g
$ http-server -p 3000
$ ngrok http 3000
$ open http://localhost:3000

検証用ページ

about:blank parentValue: PARENT
http://localhost:3000/ childValue: CHILD
http://localhost:3000/iframe.html parentValue: PARENT
message from http://localhost:3000: aaaa
https://*****.ngrok.io/iframe.html SecurityError: Blocked a frame with origin "https://*****.ngrok.io" from accessing a cross-origin frame.
https://*****.ngrok.io/iframe.html window.frameElement is null
message from http://localhost:3000: 
message from https://*****.ngrok.io: aaaa

about:blankは表示ページと同じオリジンになるので、srcが同じオリジンの場合の挙動と同じでparentの値やiframeを参照して書き換えることもできる。 Web広告の標準規格を定めるIABBest Practices for Rich Media Adsで言及されているFriendly IFrame(FIF)は about:brankのiframeの中に、対象スクリプトをsrcとしたscriptタグを入れてinDapIF = trueを定義してFIF内で実行されていることを通知するというもの。これによって広告のスクリプトにdocument.write()のようなページの描画に問題を起こし得るコードが含まれていても、iframeで影響を分離しながら 計測や表示領域の拡大縮小などはさせることができる。裏を返せばやろうと思えば何でもできるので悪意のあるスクリプトに対する防衛策にはならない。

オリジンが異なる場合は値を参照しようとするとブロックされる。 ただしparentのwindow自体は参照できるのでpostMessage()で外側とやりとりすることはできる。 これを利用したのが同じくIABが定めたSafeFrameで、 iframeを異なるオリジンにして直接外側に干渉させず、SafeFrame APIを介して拡大などさせることでセキュアにする。

ブラウザのwindow間の値渡し - sambaiz-net

参考

SafeFrame ver1.1 仕様読解、媒体側の実装例