【実務・中級編】 DOMベースXSSの特定とクライアントサイドの脆弱性診断 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

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 ではいけないのか?」と問いかけてください。その小さな疑念が、重大なインシデントを未然に防ぐ鍵となります。

現場からは以上です。次のデプロイも、安全かつ堅牢に行いましょう。

コメント

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