【実務・中級編】DOM-based XSSの発生メカニズムとソース・シンクの特定 – アプリケーションセキュリティ & 安全な開発防御ガイド

「URLのゴミ」が実行権限に化ける:DOM-based XSSの正体と実戦的な封じ込め

現場でインシデント対応をしていると、「XSSなんて今さら」と高を括っている若手が必ずと言っていいほど躓くのが DOM-based XSS だ。サーバーサイドでバリデーションを完璧に組んでいても、クライアントサイドでJavaScriptが「信頼してはいけない入力」を「実行エンジン」に流し込んだ瞬間、防御壁は砂上の楼閣と化す。

今日は、教科書的な説明はすっ飛ばして、なぜDOM-based XSSが「見えない脅威」なのか、そしてどうやってコードレベルで息の根を止めるのかを叩き込む。

—

1. なぜDOM-based XSSは防ぎにくいのか?

反射型(Reflected)や蓄積型(Stored)のXSSがサーバーサイドのログに残るのに対し、DOM-based XSSはブラウザの中で完結する。サーバー側から見れば、単なる「静的なHTML」と「読み込まれるJS」が配信されているだけで、悪意のあるペイロードはサーバーのログにすら残らないことが多い。

攻撃者の視点:どこが「盲点」か

攻撃者は、URLのフラグメント(#以降)やlocation.searchを執拗に狙う。これらはサーバーに送信されないため、WAFやサーバー側の入力バリデーションをすり抜ける。

攻撃のPoC(概念実証):

// 脆弱なサイト: https://example.com/app.html
// 攻撃URL: https://example.com/app.html#

// サイト内のJSでこんな書き方をしていたら即死する
const fragment = decodeURIComponent(window.location.hash.substring(1));
document.getElementById(‘display’).innerHTML = fragment;

innerHTMLにURLパラメータをそのまま流し込む。これが「ソース(危険な入力源)」から「シンク(実行ポイント)」への最短ルートだ。

—

2. 「シンク」を特定し、無力化する

DOM-based XSSを撲滅する唯一の道は、「危険なシンクにデータを渡すな」に尽きる。

避けるべき「危険なシンク」たち

  • innerHTML, outerHTML
  • document.write()
  • eval(), setTimeout(), setInterval()(文字列を渡す場合)
  • location.href への直接代入(javascript:スキームを含む場合)

—

3. 実務で使える「コピペ可能な」セキュア実装

「動的なコンテンツの挿入」が必要な場合、innerHTMLの代わりにtextContentやinnerTextを使うのが鉄則だ。これらはブラウザ側で自動的にエスケープされる。

安全な実装例: ユーザー入力を安全にレンダリングする

/

  • セキュアなテキスト挿入関数
  • @param {string} selector ターゲット要素のID
  • @param {string} userInput ユーザーからの入力値

/
function safeRender(selector, userInput) {
const element = document.getElementById(selector);
if (!element) return;

// NG: element.innerHTML = userInput;
// OK: textContentを使うことで、HTMLタグをただの文字列として処理する
element.textContent = userInput;
}

// URLフラグメントを安全に扱う例
const hashValue = window.location.hash.substring(1);
safeRender(‘welcome-message’, こんにちは、${hashValue}さん!);

どうしてもHTMLとして挿入する必要がある場合は?

DOMPurifyのような堅牢なライブラリを使い、サニタイズを徹底する。自作の正規表現置換で防ごうとするのは、セキュリティのプロから見れば「自殺行為」だ。

// DOMPurifyを利用した安全なHTML挿入
//

const dirtyInput = “Hello“;
const cleanHTML = DOMPurify.sanitize(dirtyInput);

document.getElementById(‘content’).innerHTML = cleanHTML;
// 結果: Hello だけが残り、悪意あるタグは削除される

—

4. 多重防壁:CSP(Content Security Policy)でトドメを刺す

コードのミスは必ず起きる。だからこそ、多重防壁が必要だ。CSPを設定すれば、仮に脆弱なコードが紛れ込んでも、インラインスクリプトの実行を阻止できる。

NginxでのCSP設定例

サーバー側で以下のヘッダーを送信し、インラインスクリプトの実行を禁止する。

信頼できないソースからのスクリプト実行を禁止し、インラインスクリプトを無効化
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’;”;

  • script-src 'self': 自分のドメインにあるJSファイルしか実行させない。
  • 'unsafe-inline' を含めないことがDOM-based XSS封じ込めの鍵だ。

—

チーフからのアドバイス

DOM-based XSSを「修正」することよりも、「脆弱な関数を使わない設計」をチームのカルチャーにすることの方が遥かに重要だ。

1. 「innerHTML」を見つけたら即座にコードレビューで指摘する。
2. 「URLパラメータは常に攻撃者のもの」という前提で実装する。
3. CSPを厳格に適用し、開発環境から「セキュリティのガードレール」を意識させる。

技術は常にアップデートされるが、脆弱性の本質は変わらない。「信用してはいけないものを、実行してはいけない場所に渡さない」。これだけ守れば、君たちの書くコードは一気に堅牢になる。次のデプロイから、まずはinnerHTMLの撲滅を始めてみてくれ。

コメント

タイトルとURLをコピーしました