DOMベースXSS:クライアントサイドに潜む「見えない」侵入経路とアーキテクチャ的防衛
多くのエンジニアが「XSS対策はサーバーサイドでエスケープすれば万全だ」と信じている。しかし、我々が扱うモダンなフロントエンドスタックにおいて、その考えはもはや幻想に近い。サーバーを一切介さず、ブラウザ上のJavaScriptの実行フローが自滅的に攻撃を成立させる「DOMベースXSS」は、WAFやサーバーサイドのバリデーションをいとも簡単にすり抜ける。
今回は、この脆弱性の深淵を覗き、アーキテクトが設計時に組み込むべき防御の要諦を解説する。
—
1. 脆弱性の解剖:サーバーのログには残らない「実行フロー」の汚染
DOMベースXSSの根本的な問題は、「データの流入元(Source)」から「危険な実行関数(Sink)」までのパスが、ブラウザのメモリ空間内で完結している点にある。
典型的な攻撃パターンは、URLのフラグメント(#以降)を利用するものだ。これはサーバーに送信されないため、ロードバランサーやWAFの検査対象外となる。
攻撃のメカニズム
攻撃者は以下のようなURLを標的に送りつける。
https://victim.com/#
開発者が書いた「何気ない」JavaScriptが、このDOM操作の引き金となる。
// 脆弱な実装例:URLフラグメントを取得し、そのままDOMに挿入している
const fragment = window.location.hash.substring(1);
const container = document.getElementById(‘content’);
// ここがSink(危険な関数)。innerHTMLはパース時にスクリプト実行を許容する
container.innerHTML = decodeURIComponent(fragment);
このコードは、ブラウザのDOMツリー構築プロセスにおいて、フラグメントを「データ」ではなく「コード」として解釈させてしまう。パケット構造を解析しても、サーバー側にはフラグメントが含まれないため、インシデント発生時のフォレンジックでさえ、この攻撃痕跡を見つけることは極めて困難だ。
—
2. 根本的な防御:Sinkの制御と信頼の境界線
DOMベースXSSを防ぐには、コードレビューの段階で「どこにデータが流れているか」を厳格に追跡する必要がある。
対策1:危険なSinkの排除
innerHTMLやouterHTML、document.writeといった、文字列を即座にDOM構造へ変換するプロパティの使用は、アーキテクチャ的に禁止すべきだ。代わりに、ブラウザがデータとして安全に扱うプロパティを強制する。
// 安全な実装:textContentを使用することで、入力は常にプレーンテキストとして扱われる
const fragment = window.location.hash.substring(1);
const container = document.getElementById(‘content’);
// 文字列として挿入されるため、タグとして解釈されない
container.textContent = decodeURIComponent(fragment);
対策2:Trusted Types APIによる強制力
モダンブラウザが提供する「Trusted Types API」は、チーフホワイトハッカーとして推奨する最強の防御層だ。これにより、危険なSinkに「型」を要求し、未検証の文字列が関数に渡されるのをブラウザレベルで阻止できる。
// CSPヘッダーでTrusted Typesを有効化する
// Content-Security-Policy: require-trusted-types-for ‘script’;
// 開発側で信頼できる型を定義する
const policy = trustedTypes.createPolicy(‘default’, {
createHTML: (input) => DOMPurify.sanitize(input) // DOMPurifyでサニタイズを強制
});
// 以降、innerHTMLへ直接文字列を代入するとブラウザが例外を投げる
container.innerHTML = policy.createHTML(userInput);
—
3. 次世代の防衛:プロンプトインジェクションと攻撃の多層化
現在、生成AIを組み込んだアプリケーションが増加しているが、ここで注意すべきは「LLMが生成した出力結果」をそのままDOMに挿入するリスクだ。
AIがプロンプトインジェクションにより悪意あるHTMLを生成し、それをフロントエンドが信頼してレンダリングすれば、それは即座にDOMベースXSSとなる。
- ガードレイルの設計: LLMからのレスポンスをフロントエンドに渡す際、必ず中間層で構造化データ(JSON)として検証し、クライアントサイドでは「データバインディング」ライブラリ(ReactのJSXなど)のオートエスケープ機能を信頼しつつ、最終的なDOM操作にはTrusted Typesを介在させる二重の防衛策が必要だ。
—
4. 監査とインシデントハンドリングの視点
セキュリティアーキテクトに求められるのは、単なるパッチ当てではなく、脆弱性が生まれない「構造」を定義することだ。
1. 静的解析(SAST)の強化: innerHTMLやevalの使用箇所をCI/CDパイプラインで自動検出し、ビルドを失敗させる。
2. 動的解析(DAST/IAST): 実行時のデータフローを追跡し、SourceからSinkへの汚染(Taint)を検知するテストを自動化する。
3. CSPの徹底: unsafe-inlineを排除し、nonce(ナンス)ベースの厳格なCSPを適用することで、万が一XSSが発生してもペイロードの実行を阻止する。
DOMベースXSSは、Webアプリケーションの境界が「サーバー」から「クライアント」へ完全に移行したことによる歴史的必然とも言える。コードの表面を撫でるのではなく、ブラウザのレンダリング・エンジンという低レイヤの挙動を理解し、その上で安全な「型」を定義すること。それが、我々エンジニアが守るべき最後の防衛線だ。
コメント