DOMベースXSS:サーバーログをすり抜ける「クライアントサイドの亡霊」を狩る
多くの開発者が、XSS対策といえば「サーバーサイドでの入力値エスケープ」だと信じ込んでいる。だが、現代のSPA(Single Page Application)において、その認識は脆弱性の温床でしかない。
サーバーのWAFがどれほど強固でも、ログ分析ツールがどれほど洗練されていても、DOMベースXSS(DOM-based Cross-Site Scripting)は、クライアントのメモリ上で静かに完結する。HTTP通信のパケット構造すら汚染しないこの攻撃は、攻撃者のブラウザと被害者のブラウザという、閉じた空間で増殖する「亡霊」のような存在だ。
今日は、この「見えない脆弱性」の本質と、アーキテクトが実装すべき真の防衛ラインについて、現場の泥臭い知見を交えて深掘りしていく。
—
1. 脆弱性の解剖:なぜ「サーバー」は無力なのか
DOMベースXSSの根本原因は、データの「信頼境界線」がクライアントサイドにシフトしたことにある。
従来のXSSは、サーバーが生成したHTMLに悪意あるスクリプトが混入する。しかしDOMベースXSSは、サーバーは単なる「静的なJSファイル」を配るだけで、動的なレンダリングの全責任がブラウザ側のJavaScriptに委ねられていることに起因する。
攻撃のメカニズム
攻撃者はURLのフラグメント(#以降)やlocation.searchを操作する。サーバー側には決して送信されないこれらのデータは、ブラウザのメモリ領域で処理され、そのままDOMの「シンク(Sink)」へと流し込まれる。
// 脆弱な実装例:URLのクエリパラメータを直接DOMに流し込む
const params = new URLSearchParams(window.location.search);
const userProfile = params.get(‘name’);
// 危険なシンク:innerHTMLにそのまま値を代入すると、HTMLパーサーが解釈してしまう
// ?name= で即死
document.getElementById(‘profile’).innerHTML = こんにちは、${userProfile}さん;
このとき、サーバーのWAFやIDSは「?name=」以降のフラグメントやパラメータを感知できないケースが多い。なぜなら、これらはHTTPリクエストのペイロードとしてサーバーサイドのログを通過しないからだ。
—
2. 防衛のアーキテクチャ:シンクを物理的に遮断する
「入力値のバリデーション」だけでこの問題を防ごうとするのは、蟻の穴を指で塞ぐようなものだ。真の対策は、「危険なシンク」をコードベースから駆逐することに他ならない。
A. 危険なシンクの使用禁止と代替
innerHTML、outerHTML、document.write()は、セキュリティの観点からは「レガシーな爆弾」である。可能な限り、以下の代替手段を用いるべきだ。
textContent: データをテキストとしてのみ扱う。HTML解釈が完全に無効化される。createElement+setAttribute: DOMツリーを安全に構築する。
// 安全な実装例
const nameDiv = document.createElement(‘div’);
nameDiv.textContent = userProfile; // テキストとして処理されるため、タグは注入不可
document.getElementById(‘profile’).appendChild(nameDiv);
B. Trusted Types APIの導入(最先端のガードレイル)
現在のモダンブラウザが提供する最強の防衛策がTrusted Typesだ。これは、危険なシンクに「信頼できる型」を通さない限り、ブラウザ側で実行をブロックする強制的なガードレイルである。
// CSP(Content Security Policy)でTrusted Typesを有効化する
// Content-Security-Policy: require-trusted-types-for ‘script’;
// ポリシーを定義して、危険な代入を制限する
const policy = trustedTypes.createPolicy(‘default’, {
createHTML: (input) => {
// ここにサニタイズ処理を強制する(DOMPurify等を使用)
return DOMPurify.sanitize(input);
}
});
// 以降、innerHTML等に生の文字列を代入しようとすると、ブラウザが例外を投げる
document.getElementById(‘profile’).innerHTML = “安全な値のみ通過可能“;
—
3. 監査とインシデントハンドリングの視点
チーフホワイトハッカーとして、私はコードレビューやペネトレーションテストにおいて、以下の観点を常に重視している。
1. データフロー解析の自動化:
grepでinnerHTMLを探す時代は終わった。静的解析ツール(SAST)をCI/CDパイプラインに組み込み、location.やdocument.referrer等のソース(Source)から、innerHTML等のシンク(Sink)までが汚染されていないか、データフローグラフを追跡させる必要がある。
2. 通信プロトコルとブラウザ仕様の乖離:
最近では、ブラウザの仕様変更により、特定の文字エンコーディングやURLパーサーの挙動が微妙に異なることで、WAFのフィルタリングを回避する技術が登場している。攻撃者はプロトコルの隅をつつき、パーサーの解釈差異を突いてくる。これに対抗するには、個別のハックではなく、ブラウザ側のサンドボックスを強化するCSPの厳格な適用こそが、唯一の正解だ。
3. 生成AIとプロンプトインジェクションへの応用:
現在、LLMをフロントエンドに組み込むケースが増えているが、LLMが生成した回答をそのままinnerHTMLに流し込むのは、DOMベースXSSの「特級呪物」だ。LLMの出力は常に「信頼できない外部データ」として扱い、必ずクライアント側で厳格なサニタイズを行うこと。
—
最後に:セキュリティは「信頼の設計」である
DOMベースXSSの本質は、エンジニアが「ブラウザのメモリなら安全だ」という甘い期待を抱くことにある。だが、そのメモリこそが攻撃者の最大の遊び場だ。
我々テックリードがすべきは、単に脆弱性を塞ぐことではない。「何が信頼できるデータで、何が危険なデータか」を、コードの構造そのもので強制するアーキテクチャを作り上げることだ。
コードは嘘をつかない。だが、開発者の甘い期待は常に裏切られる。技術的負債を抱え込む前に、今日からinnerHTMLを検索し、一つずつ排除していくことから始めてほしい。それが、世界最高峰のエンジニアリングチームへの第一歩だ。
コメント