DOMDOMタイムス#19: iframeを新たに作りさえすれば新鮮とれたてプロトタイプが手に入るの?
ひさしぶりのDOMDOMタイムスです!ブログ移行後は初めてです👶
過去のDOMDOMタイムスはZennの方にありますので、読んでみたい人は読んでみてください!
今回は、以前にDOMDOMタイムスで紹介した内容についてアップデート的な感じをやりたいと思います。
新鮮とれたてプロトタイプってなんすか?iframe作るってどういうことすか?
問題意識や前提知識に関するすべての説明を下記の記事に託して、この部分はすっ飛ばさせてください!
本稿で話されていることが「なにいってだこいつ」状態だったら、下記の記事を読んでみてくださいネ。
ちなみに、この記事を出したあとにプロトタイプ汚染周りのECMAScript提案について色々まとめる機会があったので、よければ読んでみてください。スライドでもよければ日本語版があります👶
iframeを新たに作りさえすれば新鮮なプロトタイプが必ず手に入るのか?
本題に入りましょう。結論から言うと、こんな記事を書くくらいなのでNOです。
もうすこしちゃんと言うと、createElementやcontentWindowの挙動が特定のやり方で変更されている状況においては、新鮮なプロトタイプの入手が不可能になります。
まずは素直に: createElementを汚染されるとダメ
iframeを作ってプロトタイプを取り出すコードは、たとえば下記のようなものです(さっき紹介した自分の記事から引用)。
const iframe = document.createElement('iframe')
// iframeはDOMツリーに接続して初めてDocumentを作ってくれるので接続する
iframe.style.display = 'none';
document.body.insertAdjacentElement('beforeend', iframe);
// iframeから取ってくる
const originalPromise = iframe.contentWindow.Promise
// iframeを消す
iframe.remove()
これは残念ながらcreateElementに爆弾を仕込まれるとダメです!
つまりcreateElement自体に「iframeを作ったら、そのiframeにPromiseを書き換えるようなloadイベントを仕込んでね〜」と教えておかれると、作ったばかりのiframeからでさえ新鮮なPromiseを取り出すことはできなくなってしまいます。具体的な爆弾製造装置として下記のようなスクリプトが挙げられます。
const originalCreateElement = Object.getOwnPropertyDescriptor(
document.__proto__.__proto__,
"createElement",
).value;
Object.defineProperty(document.__proto__.__proto__, "createElement", {
...Object.getOwnPropertyDescriptor(
document.__proto__.__proto__,
"createElement",
),
value: function (tagName, options) {
const element = originalCreateElement.call(this, tagName, options);
if (tagName === "iframe") {
element.addEventListener("load", () => {
try {
element.contentWindow.Promise = "I am polluted...!";
} catch (e) {
// iframe might be cross-origin
}
});
}
return element;
},
});
実際、やってみるとこんな感じです👶キャー!(トムブラウン)
iframeからでさえも新鮮なPromiseが取り出せなくなっていますねえ。
ちなみに参考までに、何もされていないコンテキストで普通にうまくいくときの様子がこちらです。
もう少しちゃんと: contentWindowを汚染されるとダメ
createElementを経由しないでiframeを作れば邪魔されずになんとかなるでしょうか?
innerHTMLを使ってiframeをつくり、そこから新鮮プロトタイプを取り出すことを考えます。
const div = document.createElement("div");
div.id = 'prototype_fountain_parent'
// iframeを追加して、そのままPromiseを取得する。終わったらdivごと消える。
// innerHTMLでscriptタグを差し込んでも実行されない(ブラウザ自体のXSS対策)ので回りくどいことをしています👶
const script = `
originalPromise = prototype_fountain.contentWindow.Promise;
prototype_fountain_parent.remove();
`
div.innerHTML = `
<iframe id="prototype_fountain" style="display:none;"></iframe>
<img src onerror="${script}">
`;
document.body.insertAdjacentElement('beforeend', div);
ひとまず、これはこれでちゃんとワークすることが確認できます :)
(もちろんWebページのCSP設定次第で動かないことがあります!)
では、contentWindowが汚れていたらどうなるでしょうか。下記を先に実行しておきます。
const originalContentWindow = Object.getOwnPropertyDescriptor(
HTMLIFrameElement.prototype,
"contentWindow",
).get;
Object.defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
...Object.getOwnPropertyDescriptor(
HTMLIFrameElement.prototype,
"contentWindow",
),
get() {
const contentWindow = originalContentWindow.call(this);
if (!contentWindow) {
return contentWindow;
}
contentWindow.Promise = "i am polluted, again...!";
return contentWindow;
},
});
はい、残念ですがこのスクリプトによりinnerHTML作戦も失敗するようになります。うむうむ。
なんなら、さっきのcreateElement作戦もcontentWindowを経由しているので、このスクリプトで失敗させられます。
たぶんですが、ここから先はイタチごっこです。
なんかもっと作戦あるかもですが、たぶん色々邪魔されて新鮮なプロトタイプの買い付けは失敗に終わると思います😂
でももし面白いパターンがあったら教えてください!:)
iframeからの新鮮プロトタイプ入手をセキュリティ対策としてやっている人は注意してね
iframeからの新鮮プロトタイプ入手をセキュリティ対策としてやっている方は注意してください!
つまり、例えば「この機密情報をフロントエンドでサクッと扱いたいんだけど、もしもプロトタイプ汚染があったら簡単に盗まれるよな。うんうん、一旦iframeを作って、新鮮なプロトタイプで処理しよう。そうすれば安全だろう」という発想です。
以下、立場を分けてもう少し具体的に考えてみましょう。
1. あなたがwebページ開発者なら
webページで使われているライブラリは容易にiframeの「内部」を汚染できます。これはサプライチェーンアタックの文脈で特に現実的な話です(膨大な依存関係の中にいるライブラリがある日、プロトタイプを汚染してくる可能性があるというわけ)。
もしiframeからの新鮮プロトタイプ入手をそれに対するセキュリティ対策としてやっていたら、それはセキュリティ対策として成立しないので注意してください👶サプライチェーンアタックに対する対策を徹底することが必要だと思います。
2. あなたが拡張開発者なら
ちなみに拡張の開発者の方も注意してください!
「いやうちのスクリプトはdocument_startで誰よりも早く動くから……」と思っていませんか?
少なくともChromiumでは、新しく作られたiframeについて、その中でdocument_startのcontent scriptが動くよりも先に親コンテクストがiframeコンテクストのプロトタイプを操作できることがわかっています。
つまり、ページでiframeが新たに作られたときにそのiframeの中のmain worldであなたの拡張が何か機密を扱うスクリプト処理を行うようになっている場合、まずいということです。
このChromiumのバグは直された方がいいんじゃないかなと思いますが、ずっと放置されているので多分このままです😂<-笑っている場合ではない
誰かに新鮮なプロトタイプを使ってほしくないと思っている人へ(攻撃者に限らず、たまにいる)
この記事のテクニックを使ってみてください!
ということで
今日のDOMDOMタイムスはここまでです👶
ちょっとマニアックな話でしたが久々に書けてよかったです!
ちなみにAgoricのSESは、このプロトタイプ汚染によるセキュリティ問題を強く意識したプロジェクトです。興味がある人は見てみてください。
また、再掲になりますがSES周辺のES提案については下記(スライドなら日本語版があります)を読んでみてください!
なお、、、
- 「iframeからプロトタイプがとれるよ〜」と書いた自分の前の記事には、この記事へのリンクを貼っておきました👶
- OWASPのブラウザ拡張チートシートにこの件を書いてみようと思います✌️(https://github.com/OWASP/CheatSheetSeries/issues/1713)