DOMベースXSS:クライアントサイドに潜む「見えない」脅威と、その完全封じ込め戦略
こんにちは。現場の最前線でインシデント対応やペネトレーションテストを主導しているセキュリティエンジニアです。
最近のWebアプリケーション開発では、ReactやVue.jsのようなフレームワークのおかげで、サーバーサイドでゴリゴリとHTMLを生成する時代から、クライアントサイドで動的にDOMを操作する時代へ完全に移行しました。しかし、この「動的なDOM操作」こそが、サーバー側のWAFをいとも簡単にすり抜ける「DOMベースXSS」の温床であることを忘れてはいけません。
今日は、教科書的な説明はすっ飛ばして、攻撃者がどこを見て、我々はどう守るべきか。その「泥臭い現実」を共有します。
—
1. DOMベースXSSの「真の姿」を理解する
従来のXSS(反射型や蓄積型)は、ペイロードがサーバーを経由します。サーバーのログに残り、WAFで検知可能です。しかし、DOMベースXSSは違います。悪意のあるデータはブラウザの中で完結し、サーバーには一切送信されません。
攻撃の構造はシンプルです。
1. ソース(Source): 攻撃者が操作可能な入力(location.hash, location.search, document.referrer など)。
2. シンク(Sink): 実行されると危険な関数(innerHTML, document.write, eval, setTimeout など)。
攻撃者は、URLのハッシュフラグメントにJavaScriptを仕込み、それがJavaScript経由でinnerHTMLに流し込まれるのを虎視眈々と待っています。
攻撃者視点のPoC(実例)
例えば、以下のようなコードがサイトにあったとします。
// 脆弱なコード例:URLのパラメータをそのままDOMに挿入している
const params = new URLSearchParams(window.location.search);
const userProfile = document.getElementById('profile');
userProfile.innerHTML = "ようこそ、" + params.get("name") + "さん";
攻撃者は以下のURLを被害者に踏ませるだけで、セッションハイジャック等の攻撃を完遂します。
https://example.com/profile?name=<img src=x onerror=alert(document.cookie)>
サーバー側ではこのリクエストを認識すらできません。これがDOMベースXSSの恐ろしさです。
—
2. 防御の鉄則:シンクへの入力を「信頼しない」
DOMベースXSSを根絶する唯一の道は、「データの出どころを問わず、危険なシンクに渡す際は必ず無害化する」こと、そして「そもそもシンクの利用を避ける」ことです。
セキュアな実装パターン
1. innerHTML を捨て、textContent を使う
これが最も効果的かつ根本的な解決策です。textContent は入力を文字列としてのみ扱うため、タグとして解釈されるリスクがありません。
// セキュアな実装例
const params = new URLSearchParams(window.location.search);
const userProfile = document.getElementById('profile');
// .innerHTMLではなく.textContentを使用する
// これにより、攻撃者が<script>タグを含めても、単なるテキストとして表示される
userProfile.textContent = "ようこそ、" + (params.get("name") || "ゲスト") + "さん";
2. DOMPurifyによるHTMLサニタイズ
どうしても innerHTML を使わなければならない(リッチテキストエディタ等)場合は、信頼性の高いライブラリ DOMPurify を必ず使用してください。
// DOMPurifyを利用した安全なHTML挿入
import DOMPurify from 'dompurify';
const unsafeData = params.get("comment");
const cleanData = DOMPurify.sanitize(unsafeData); // 悪意のあるタグを削除
document.getElementById('comment-box').innerHTML = cleanData;
—
3. インフラレベルでの二重防壁(CSPの導入)
アプリ側のコード修正が大前提ですが、万が一のバグを見越して、ブラウザ側で「怪しいスクリプト」を動かさないように制限をかけるのが、プロのインフラ構築です。
NginxやWebサーバーのレスポンスヘッダーに Content-Security-Policy (CSP) を設定してください。
# Nginx設定例:信頼できるスクリプト以外は一切実行させない
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';";
default-src 'self': 読み込み元を自分のドメインに制限。script-src 'self': インラインスクリプトや外部の怪しいスクリプトの実行を禁止。object-src 'none': Flashなどの古い脆弱なプラグインを無効化。
—
最後に:エンジニアとして生き残るために
セキュリティ対策に「銀の弾丸」はありません。しかし、DOMベースXSSに関しては、「外部からの入力に触れるときは、常にそのデータが毒を持っていると仮定して扱う」という防衛的なコーディング習慣が、あなた自身とあなたのプロダクトを守ります。
レビュー時に innerHTML という文字列を見つけたら、即座に「なぜ textContent ではいけないのか?」と問いかけてください。その小さな疑念が、重大なインシデントを未然に防ぐ鍵となります。
現場からは以上です。次のデプロイも、安全かつ堅牢に行いましょう。
コメント