【テクニカル・上級編】DOMベースXSSの発生メカニズムとクライアントサイドの脆弱性 – アプリケーションセキュリティ & 安全な開発防御ガイド

DOMベースXSS:クライアントサイドの深淵と「信頼」の再構築

「サーバーを通らないから安全だ」。かつて多くのエンジニアがそう嘯いたこの言葉こそが、今やDOMベースXSS(DOM-based Cross-Site Scripting)という亡霊を招き寄せる呪文となっている。

サーバーサイドのログに痕跡を残さず、WAFの防御網をすり抜ける。DOMベースXSSは、現代のSPA(Single Page Application)において、ブラウザのメモリ空間を直接汚染する「最も静かなる侵入者」だ。今日は、この脆弱性の本質を、単なる「入力値のサニタイズ不足」という表面的な議論から引き剥がし、ブラウザの実行コンテキストという低レイヤの視点から解剖していく。

—

1. 脆弱性の本質:ブラウザという名の「無防備な実行環境」

DOMベースXSSがサーバーサイドのXSSと根本的に異なる点は、その「攻撃の起点(Source)」と「実行点(Sink)」が、完全にクライアントのメモリ上で完結していることにある。

例えば、location.hash や location.search を介して受け取った文字列を、何の検証もなしに innerHTML や document.write() に流し込むコード。これは、攻撃者がURLのフラグメントを書き換えるだけで、ブラウザがJavaScriptの実行パスを改ざんできることを意味している。

なぜこれが防げないのか? それは、開発者が「JavaScriptの文字列操作は安全なサンドボックス内で行われている」という幻想を抱いているからだ。しかし、ブラウザのパーサーは、悪意あるスクリプトが挿入されたDOMノードを、それが「外部からの入力」であるか「プログラムの定数」であるかを区別せず、等しく実行可能なバイナリとして解釈する。

—

2. 攻撃のメカニズム:Sinkの脆弱な挙動

攻撃者は、ブラウザの仕様の隙を突く。例えば、以下のようなコードは致命的だ。

// 脆弱な例:URLハッシュから直接ソースを取得し、DOMに挿入している
const userBio = decodeURIComponent(window.location.hash.substring(1));
document.getElementById(‘profile-bio’).innerHTML = userBio;

ここで攻撃者が #! というURLを作成すれば、innerHTML はこの文字列をHTMLとしてパースし、onerror イベントを即座に発火させる。

ここで意識すべきは、「どのAPIがSink(シンク)になり得るか」というブラックリストを暗記することではない。データが「不透明な状態」から「実行コンテキスト」へ移行するすべての境界線を、アーキテクチャレベルで監視することだ。

—

3. チーフアーキテクトが導く防御戦略:信頼の境界線

DOMベースXSSを撲滅するためには、従来の「エスケープ処理」といった小手先の対策を超え、「Trusted Types」という最新のブラウザ仕様を強制するアーキテクチャが必要だ。

Trusted Typesによる「動的実行」の封じ込め

Trusted Typesは、DOMのSink(innerHTML 等)に対して、未検証の文字列を直接渡すことを禁止する仕組みだ。これにより、開発者は「型付けされた安全なオブジェクト」のみをDOMに流し込むことが義務付けられる。

// セキュリティポリシーの定義(CSP等で設定)
const policy = trustedTypes.createPolicy(‘my-policy’, {
createHTML: (input) => {
// ここでDOMPurify等を用いた厳格なサニタイズを行う
return DOMPurify.sanitize(input);
}
});

// 安全な方法:ポリシーを介さないと実行時に例外がスローされる
const userBio = decodeURIComponent(window.location.hash.substring(1));
document.getElementById(‘profile-bio’).innerHTML = policy.createHTML(userBio);

このアプローチは、アプリケーションコードを強制的にセキュアな設計へと誘導する。もはや「エスケープ漏れ」による脆弱性は、型システムによってコンパイル時(または実行時)に検出可能となる。

—

4. 生成AI時代の新たな脅威:プロンプトインジェクションとの交差点

我々が直面しているのは、単なるXSSだけではない。LLMを組み込んだフロントエンドアプリケーションにおいて、ユーザーからの入力をプロンプトとして利用する場合、DOMベースXSSは「プロンプトインジェクション」と結託する。

攻撃者がURL経由で埋め込んだ悪意ある指示が、DOMを汚染し、さらにLLMの推論プロセスへと流れ込む。ここでの防御層(ガードレイル)は、単一のバリデーションでは不可能だ。

  • 入出力の二重検閲: フロントエンドでのサニタイズ(DOMPurify)と、LLMのプロンプトを処理するバックエンドでのエンコーディングを分離すること。
  • コンテキストの分離: ユーザー入力とAIが生成した出力をHTMLタグで厳格に分離(