DOM型XSSの深淵:サーバーログに残らない「見えない攻撃」をいかに解体するか
多くのエンジニアが「XSS対策はサーバーサイドでサニタイズすれば完璧だ」と信じ込んでいる。しかし、それは脆弱性の氷山の一角しか見ていない。サーバーサイドが完璧にクリーンであっても、ブラウザ内部で完結する「DOM-based XSS」が残っていれば、あなたのアプリケーションは無防備なまま侵入を許すことになる。
今日は、プロトコル層の通信すら発生させず、単なるURLフラグメントの書き換えで実行されるこの「ゴースト」を、アーキテクトの視点からどう解体し、封じ込めるかを論じる。
—
1. DOM型XSSの本質:データフローの「切断」と「再構成」
DOM型XSSが従来の反射型や格納型と決定的に異なる点は、「サーバーのレスポンスには脆弱性となるペイロードが含まれない」という事実だ。
攻撃のフローは、クライアントサイドのJavaScriptが location.hash や location.search などの「ソース(Source)」からデータを読み取り、それを innerHTML や document.write といった「シンク(Sink)」へ渡すことで成立する。
脆弱性の成立メカニズム
// 脆弱な実装例:URLフラグメントから動的にコンテンツを生成
const fragment = decodeURIComponent(window.location.hash.substring(1));
// 攻撃者が # を送れば即座に実行される
document.getElementById(‘content’).innerHTML = fragment;
このコードの根本的な問題は、「外部からの入力を、ブラウザがDOMツリーを構築するための命令として信頼してしまっている」という点にある。サーバーのWAFやIDSは、HTTPリクエストのボディを見ているため、フラグメントがブラウザのエンジンに届く前に遮断することはできない。
—
2. 解析手法:ソースからシンクへのトレース(Taint Analysis)
インシデントハンドリングの現場では、静的解析ツール(SAST)だけで満足してはいけない。私は、ブラウザのデベロッパーツールを使い倒した「動的データフロー追跡」を推奨する。
監査のステップ
1. Sourceの特定: window.location, document.referrer, window.name など、ユーザーが介入可能なJavaScriptプロパティを洗い出す。
2. Sinkの特定: データの破壊的出力を行う箇所をマークする。innerHTML, outerHTML, document.write(), eval(), setTimeout(string) 等が該当する。
3. Execution Pathの追跡: 実行コンテキストの切り替わりを追跡し、信頼境界線(Trust Boundary)がどこで越境されているかをマッピングする。
特に、モダンなフレームワーク(ReactやVue)であっても、dangerouslySetInnerHTML や v-html を不用意に使用していれば、アーキテクチャの根幹が揺らぐことになる。
—
3. 防衛アーキテクチャ:ガードレイルの設計
DOM型XSSを完全に封じ込めるには、コードレベルの修正だけでなく、ブラウザのセキュリティ機能を強制する「防衛層」が必要だ。
A. 厳格なCSP(Content Security Policy)の実装
CSPは、攻撃者が悪意あるスクリプトを注入しても、それを実行させないための最強のガードレイルだ。特に script-src 'nonce-...' を使用し、インラインスクリプトを一切許可しない構成を目指すべきである。
推奨するCSP設定の例
Content-Security-Policy: default-src ‘self’; script-src ‘nonce-random123’; object-src ‘none’; base-uri ‘self’;
B. Trusted Typesの採用(最前線の防御)
現在、最も推奨されるアプローチは「Trusted Types」APIの使用だ。これは、危険なシンク(innerHTML等)に渡す前に、必ず指定されたポリシーを通すことをブラウザに強制する。
// ポリシーの定義:HTMLを安全にエスケープする
const policy = trustedTypes.createPolicy(‘default’, {
createHTML: (input) => DOMPurify.sanitize(input) // DOMPurify等で無害化
});
// 以降、innerHTMLに文字列を直接渡すとブラウザがブロックする
element.innerHTML = policy.createHTML(userInput);
—
4. チーフホワイトハッカーからの提言
技術は常に進化している。昨今の生成AIによるコード生成は、しばしば「セキュリティ上のベストプラクティスを無視した、動くが脆いコード」を出力する。これをそのままCI/CDパイプラインに流し込むのは自殺行為だ。
1. セキュリティ・バイ・デザイン: DOM操作を直接行わず、textContent などの安全なAPIをデフォルトにするようプロジェクトのコーディング規約を強制せよ。
2. 自動化された監査: GitHub Actionsなどのパイプラインで semgrep を実行し、innerHTML が出現した瞬間にビルドを失敗させる(Fail-fast)仕組みを構築せよ。
3. 人間によるレビュー: 複雑なDOM操作を伴う機能については、必ず「攻撃者の視点」を持つエンジニアによるコードレビューを通せ。
DOM型XSSは、目に見えない場所で静かに進行する。しかし、アーキテクチャのレイヤーで「信頼しない」という原則を徹底すれば、必ず防げる。防衛とは、ツールを導入することではなく、「データがどこから来て、どこで解釈されるのか」というフローを、エンジニアが完全に支配下に置くことに他ならない。
現場の泥臭い戦いこそが、最強のセキュリティを作り上げる。健闘を祈る。
コメント