🥕

DOMDOMタイムス#13: プロトタイプ汚染環境で、キレイなプロトタイプをiframeから引っこ抜く

2023/10/10に公開

3週間ぶりのDOMDOMタイムスです!!!!
先々週、先週は下記の「webページ崩し」の作成に邁進していましたので……(お気に入りだから動画も貼っちゃう)

twitterで良い反応がもらえたので、これもいつかリリースしたあかつきにはちゃんと記事にしようと思います👶
https://x.com/i_am_canalun/status/1707700748610629911

さて今日はあいだがあいてしまったのでちゃんと実践的なDOMDOMトークをしていくザマス。
(DOMってよりけっこう幅広く使える話かも??)

まとめ

  • プロトタイプはなんだかんだ汚れがち(polyfillはもちろん、ライブラリ(例:Zone.js)が汚してくることもやっぱりある)
  • プロトタイプが汚染されまくっているコンテクスト上できれいなプロトタイプが得たいなら、iframeを作ってそこから引っ張り出すといい

プロトタイプの書き換えって意外とあるヨネ

プロトタイプの書き換えが行われているwebページって意外とあるよね

例えばGoogle Cloud(https://console.cloud.google.com/)を見てみます。
PromiseDocument.prototype.querySelectorAllがガンガンに書き換えられていることがわかりますね👶
一方でShadowRootはネイティブのままっぽいです。

他のページも色々確認してみると、案外出てくるものです。

このような書き換えは、みんな大好きpolyfillによっても起こりうるし、その他のライブラリの使用によっても起こりえます。
(例えば有名なZone.jsには、自身によるPromiseの書き換えが成功したかを判定するコードがあり、なんか書き換わっているサイトがたしか前あった(忘れちゃったけど): https://github.com/angular/zone.js/blob/b11bd466c20dc338b6d4d5bc3e59868ff263778d/lib/zone.ts#L707)

汚染されていないプロトタイプをiframeから取得する

じゃあ書き換えられていないフレッシュなプロトタイプがほしいときどうしよう??

やり方

答えはカンタン、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()

さっきのGCPのページでやってみるとこんな感じになりモス👶いいね〜!

関数だけとってくる方法

ちなみに、関数だけでいいからキレイなやつがほしい!という人向けにも、一応下記のようなコードを載せておいてみます。
(documentから呼び出せるのはDocument.prototype.querySelectorAllであって、Element.prototype.querySelectorAllを引っ張ってきてもダメなことに注意!!)

const iframe = document.createElement('iframe')

// iframeはDOMツリーに接続して初めてDocumentを作ってくれるので接続する
iframe.style.display = 'none';
document.body.insertAdjacentElement('beforeend', iframe);

// iframeから取ってくる
const originalDocumentQuerySelectorAll = iframe.contentWindow.Document.prototype.querySelectorAll

// iframeを消す
iframe.remove()

// callやapplyでむりやり使っちゃおう!
// 例えばこれはdocument.querySelectorAll('div')と同じだね
originalDocumentQuerySelectorAll.call(document, 'p')

// bindで保管しておくのも場合によっては便利かも?
const documentQuerySelectorAll = originalDocumentQuerySelectorAll.bind(document)
documentQuerySelectorAll('div')

getterやsetterでもできるヨ

getterやsetterでも同じことができますが、少し工夫が必要です。

例えばElement.prototype.childrenでやってみましょう。

// iframeを作ったあと、こうやると失敗する
const originalElementChildren = iframe.contentWindow.Element.prototype.children
// => Uncaught TypeError: Illegal invocation

この書き方ではElement.prototypeを呼び出しコンテキストとしてgetterchildrenを実行していることになっちゃうんですね
いまはgetterchildrenの本体がほしいので、Object.getOwnPropertyDescriptorで無理やり抜き取りましょう🌞

const originalElementChildren = Object.getOwnPropertyDescriptor(Element.prototype, 'children').get
// => children() { [native code] }

あとはこれをcallやapplyでむりやり使えばよいね!
というわけでiframeの作成からすべてまとめて書くとこんな感じね。

const iframe = document.createElement('iframe')

// iframeはDOMツリーに接続して初めてDocumentを作ってくれるので接続する
iframe.style.display = 'none';
document.body.insertAdjacentElement('beforeend', iframe);

// iframeから取ってくる
const originalElementChildren = Object.getOwnPropertyDescriptor(iframe.contentWindow.Element.prototype, 'children').get

// iframeを消す
iframe.remove()

originalElementChildren.call(document.documentElement)

補足: 汚染されているかの判定

ところで関数の場合は、関数をiframeからぶっこ抜いてくるべきかの判断のために、関数がそもそも汚染されているかの判定があると便利かもしれないんだけど、それはこのあたりを見てみてね。

if( /\{\s+\[native code\]/.test( Function.prototype.toString.call( this[ p ] ) ) ) {
    // yep, native
}

ちなみに👆って要するに、native codeという文字列が出るかで判定しようって話なんだけど、実はこれちゃんとECMAScriptのFunction.prototype.toString()の仕様に準拠していますヨ👶
Function.prototype.toString()native codeという文字列を含んだ結果を返すのは仕様なんですねえ。

おわりに

まあそりゃ一番いいのはShadow RealmがESに実装されることなんだけどね!
でももうしばらくなさそうらしい、そんな話をTC39のイベントで耳にしました🌝
(実装がきついよって話がgithub上で議論されているらしいので、気になる人は見てみよう!!)

今日はここまで!

参考

本稿は下記の記事にインスパイアされています!

GitHubで編集を提案

Discussion