DOM-based XSSの深淵:ブラウザの実行コンテキストを支配する「ソースとシンク」の静的解析
セキュリティ界隈では、DOM-based XSSは「枯れた脆弱性」として軽視されがちだ。しかし、現代のシングルページアプリケーション(SPA)や、マイクロフロントエンドアーキテクチャにおいて、この脆弱性は依然として「特権昇格」や「セッションハイジャック」の最短ルートであり続けている。
サーバーサイドのコードをどれほど堅牢に書き上げても、ブラウザというサンドボックスの内部で、JavaScriptの実行フローが汚染されていれば、それは無意味だ。今日は、表面的なバリデーションではなく、ブラウザの実行モデルそのものを制御するアーキテクトの視点で、この問題の本質を解剖する。
—
1. 脆弱性の発生メカニズム:ソースからシンクへの「汚染された伝搬」
DOM-based XSSを理解するために重要なのは、ブラウザのメモリ上で行われるデータの「汚染伝搬(Taint Propagation)」だ。
- Source(汚染源): 攻撃者が制御可能なデータがブラウザ環境に入る地点(
location.hash,location.search,document.referrer,window.nameなど)。 - Sink(実行地点): データを実行・レンダリングする地点(
innerHTML,eval(),setTimeout(),document.write()など)。
問題の本質は、「開発者が、クライアントサイドで完結するデータフローが外部入力に依存しているという事実を、サーバーサイドのプロトコル境界と混同していること」にある。
例えば、ReactやVueなどのモダンフレームワークを使っていても、dangerouslySetInnerHTML を安易に使用したり、サードパーティのライブラリがURLパラメータを適切にエスケープせずDOMに挿入したりするだけで、セキュリティの防御層は一瞬で崩壊する。
—
2. 実践的監査:シンクの追跡と防御のアーキテクチャ
コードレビューにおいて、すべてのinnerHTMLを追いかけるのは非効率だ。我々アーキテクトがやるべきは、「ソースを特定し、その入力を無害化するまでのパイプラインを強制すること」である。
安全な実装パターン:DomPurifyによる無害化
直接DOMを操作するのではなく、一度「サニタイズ(浄化)」という中間層を必ず通す設計にする必要がある。
import DOMPurify from ‘dompurify’;
// 攻撃者が制御するURLフラグメントを取得
const userControlledInput = window.location.hash.substring(1);
// 【重要】直接シンクへ渡さず、必ずサニタイザーを通す
// 攻撃的なタグやイベントハンドラ(onerror等)を構造的に除去する
const cleanHTML = DOMPurify.sanitize(userControlledInput);
// 安全にレンダリング
document.getElementById(‘content’).innerHTML = cleanHTML;
—
3. 防御の多層化:CSPによる実行環境の制約
コードレベルの修正だけでは、将来的な脆弱性の混入を防ぎきれない。インフラ・フロントエンド設計の両面から「ガードレイル」を敷くのが、我々プロの仕事だ。
Content Security Policy (CSP) の適用
DOM-based XSSの究極的な防御策は、インラインスクリプトの実行を物理的に禁止することだ。
HTTPヘッダーでCSPを設定し、実行コンテキストを制約する
Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-random123’; object-src ‘none’; base-uri ‘self’;
このポリシーを適用すれば、たとえ攻撃者がinnerHTMLを通じてを注入しようとしても、nonce(使い捨ての乱数)が含まれていない限り、ブラウザは実行を拒否する。DOM-based XSSが成功するためには、「信頼できない入力がDOMに挿入されること」と「それが実行可能であること」の双方が必要だが、CSPはこの後者を絶つ。
---
4. プロフェッショナルへの提言:静的解析から動的フッキングへ
現代のWebアプリケーションにおいて、手動のソースコードレビューには限界がある。我々が推奨するのは、CI/CDパイプラインへの「AST(抽象構文木)解析」の導入だ。
1. 静的解析: ESLintのカスタムルールや、CodeQLを用いて、SourceからSinkへのパスがエスケープ関数を経由していないコードを自動的に検出し、ビルドを失敗させる。
2. 実行時のフッキング: 開発環境において、ブラウザのネイティブAPI(Element.prototype.innerHTMLなど)をラップし、汚染されたデータが渡された際にコンソールへスタックトレースを出力する仕組みを構築する。
結論:技術的負債としてのセキュリティ
DOM-based XSSを放置することは、ブラウザという「クライアントサイドの特権領域」への鍵を攻撃者に渡すことに等しい。
コードを書くとき、私は常に「もしこの変数に、悪意のあるJavaScriptが格納されていたら、どこで爆発するか?」を自問自答する。ソースを特定し、シンクを封じ込め、CSPで囲い込む。この泥臭い積み重ねこそが、現代のフロントエンドセキュリティにおける唯一の正解だ。
脆弱性はコードの中に潜むのではない。あなたの設計思想の「盲点」の中に、静かに息を潜めているのだ。
コメント