「WAFを入れているのに、セキュリティ診断でXSSが指摘された」。その原因がDOM型XSSである場合があります。サーバーへのリクエストが発生しないため、多くのWAFやサーバー側の検証をすり抜けます。
この記事では、DOM型XSSの仕組みと反射型・蓄積型XSSとの根本的な違い、攻撃者が悪用する「ソース」と「シンク」の概念、そして開発者・情シス担当者が今すぐ実践できる防御手順を現場目線で解説します。

DOM型XSSとは?(概要となぜ重要か)
DOM型XSS(DOM-based Cross-Site Scripting)とは、悪意のあるスクリプトがサーバーを経由せず、ブラウザ内のDOM(Document Object Model:Webページの構造を表すツリー)操作だけで実行されるタイプのXSS攻撃です。
通常のXSS(反射型・蓄積型)は、悪意のあるコードがサーバーから送り返されるHTMLに含まれます。これに対してDOM型XSSは、サーバーへのリクエストすら発生しないケースがあります。URLの#以降(フラグメント部分)に含まれるペイロードは、そもそもサーバーに届きません。
XSSの種類を整理すると次のようになります。
| 種類 | 攻撃コードの経路 | サーバーへの送信 | WAFによる検出 |
|---|---|---|---|
| 反射型XSS | URLパラメータ → サーバー → HTML | あり | 検出しやすい |
| 蓄積型XSS | DB保存 → サーバー → HTML | あり | 検出しやすい |
| DOM型XSS | URLフラグメント等 → ブラウザのDOM操作 | 発生しないケースあり | 困難 |
一般的なXSSの概要についてはXSSとは?仕組みと対策を現場目線で解説もあわせて参照ください。
攻撃の仕組み(敵を知る)
DOM型XSSを理解するには、「ソース」と「シンク」という2つの概念が不可欠です。
1. ソースとシンクとは?
ソース(Source)とは、攻撃者がコントロールできるデータの入力元です。代表的なソースには次のものがあります。
・location.hash: URLの#以降の文字列(サーバーに送信されない)
・location.search: URLのクエリパラメータ(?以降)
・document.referrer: 参照元ページのURL
・window.name: ブラウザウィンドウの名前
・postMessageイベント: クロスオリジンのメッセージ受信
シンク(Sink)とは、JavaScriptがDOMを書き換える際に使われる危険なAPIです。ここにソースからのデータが無検証で渡されると、スクリプトが実行されます。
・innerHTML / outerHTML: HTML文字列として解釈され、<script>タグやonerror属性が実行される
・document.write(): HTMLドキュメントに直接書き込む
・eval(): 文字列をJavaScriptとして実行
・setTimeout() / setInterval(): 第一引数に文字列を渡すと評価される
・href属性への直接代入: javascript:スキームを通じた実行
2. 典型的な攻撃シナリオ
次のようなJavaScriptコードが脆弱なWebアプリに存在したとします。
// 脆弱なコード例(概念説明のため掲載) var name = location.hash.substring(1); // #以降を取得 document.getElementById('greeting').innerHTML = 'こんにちは、' + name;
攻撃者は次のようなURLをターゲットに送りつけます。
# 攻撃URLのイメージ(攻撃を助長しないよう概念として示す) https://example.com/page#<img src=x onerror=悪意ある処理>
このURLにアクセスしたユーザーのブラウザ内で、location.hashから取得した文字列がinnerHTMLに渡され、onerrorイベントが発火してスクリプトが実行されます。URLのフラグメント部分(#以降)はサーバーに送信されないため、WAFはこの攻撃ペイロードを検知できません。
3. 攻撃が成立した場合に起きること
DOM型XSSが成立すると、攻撃者は次のことが可能になります。
・セッションCookieの窃取(HttpOnly属性が設定されていない場合)
・偽のログインフォームの表示によるフィッシング
・ユーザーのブラウザを踏み台にした内部システムへの不正リクエスト
・ページの改ざんによる誤情報の表示
具体的な防御手順
1. 危険なシンクを使わない(根本対策)
最も効果的な対策は、危険なシンクの使用を避けることです。innerHTMLの代わりにtextContentを使うと、入力値はHTMLとして解釈されずテキストとしてそのまま表示されます。
// NG: innerHTMLに未検証の値を渡す element.innerHTML = userInput; // OK: textContentを使う(HTMLとして解釈されない) element.textContent = userInput; // OK: DOMノードを生成して追加する const node = document.createTextNode(userInput); element.appendChild(node);
ユーザー入力を画面に表示するだけであれば、innerHTMLを使う必要はほとんどありません。まずは既存コードのinnerHTML使用箇所を洗い出すことから始めましょう。
2. DOMPurifyでサニタイズする(HTML表示が必要な場合)
リッチテキストエディタなど、どうしてもHTMLを表示しなければならない場合は、DOMPurifyなどの実績あるサニタイズライブラリを使用します。
# DOMPurifyの利用例(npmでインストール後) import DOMPurify from 'dompurify'; // サニタイズしてからinnerHTMLに渡す const clean = DOMPurify.sanitize(userInput); element.innerHTML = clean;
DOMPurifyは<script>タグやjavascript:スキーム、危険なイベントハンドラーを除去した安全なHTMLを返します。サニタイズを自前で実装すると漏れが生じやすいため、実績あるライブラリの使用を強く推奨します。
3. Content Security Policy(CSP)の設定
CSP(コンテンツセキュリティポリシー)はスクリプトの実行元を制限するHTTPヘッダーです。正しく設定すれば、DOM型XSSが成立してもスクリプトの実行をブロックできます。
# Nginxでのヘッダー設定例 add_header Content-Security-Policy "script-src 'self' 'nonce-{ランダム値}'; object-src 'none';" always; # Apache(.htaccessまたはhttpd.conf) Header always set Content-Security-Policy "script-src 'self' 'nonce-{ランダム値}'; object-src 'none';"
CSPのunsafe-inlineは危険なインラインスクリプトを許可してしまうため、使用を避けます。代わりにnonceやhashベースのアプローチを選択します。CSPを含むセキュリティヘッダーの詳細については、HTTPSだけでは足りない|セキュリティヘッダー完全ガイド(CSP・HSTS・X-Frame-Options)設定と確認方法で解説しています。
4. eval()と類似APIの禁止をLinterで強制する
コードレビューやLinterのルールでeval()・Function()コンストラクタ・setTimeout(文字列)の使用を禁止します。ESLintなどのツールで自動チェックを組み込むことが効果的です。
# .eslintrc.jsonでeval禁止ルールを設定する例 { "rules": { "no-eval": "error", "no-new-func": "error", "no-implied-eval": "error" } }
中小企業でも今日からできること
「うちは内製開発していないから関係ない」と感じるかもしれませんが、社内ポータルや外部に発注したWebシステムにもDOM型XSSが潜んでいることがあります。情シス担当者が今すぐ動ける対策を挙げます。
・外部ベンダーへの確認: 開発委託しているシステムについて、「DOM型XSSの診断を実施しているか」を確認する。脆弱性診断の範囲にDOMベースの検査が含まれているかを契約・仕様書で確認する
・ブラウザのDevToolsを使った簡易確認: URLのハッシュ部分に#<h1>test</h1>を付加してアクセスし、ページ上に「test」が見出しとして表示される場合、innerHTMLにハッシュが渡されている可能性がある
・開発チームへのルール徹底: コーディング規約にinnerHTML禁止・textContent使用を明記し、コードレビューのチェック項目に追加する
・定期的なDOMXSS診断: DOMXSSスキャナーや外部の脆弱性診断サービスを利用して、自社システムの潜在的な問題箇所を把握する
セキュリティの観点から見た安全なコード設計の原則については、セキュアコーディングとは?安全なWebアプリを作るための基本原則と脆弱性を防ぐ実装ガイドも参考にしてください。
よくある誤解と注意点
【誤解1】WAFがあればDOM型XSSは防げる
WAFはHTTPリクエスト・レスポンスを検査しますが、DOM型XSSの多くはURLフラグメント(#以降)やpostMessageのようなサーバーを経由しないデータに由来します。WAFはこれらを検査できないため、根本的な防御にはなりません。WAFはあくまで多層防御の一層として捉え、コード側での対策が必須です。
【誤解2】HTTPS化すれば安全
HTTPS(TLS暗号化)は通信路の傍受を防ぐものであり、アプリケーションの脆弱性とは別の問題です。HTTPS環境でもDOM型XSSは成立します。
【誤解3】SPAでしか発生しない
DOM操作は従来型のWebページでも行われています。jQuery時代のコード($('#target').html(input)など)でも同様の問題が発生します。SPA(シングルページアプリケーション)に限った問題ではありません。
【注意】HttpOnly CookieはDOM型XSSでのCookie窃取を防ぐが万能ではない
CookieにHttpOnly属性を設定することで、JavaScriptからのCookieアクセスをブロックできます。DOM型XSSが成立してもセッション情報の直接窃取は防げます。ただし、偽フォームの表示やUIの改ざん自体は防げないため、HttpOnlyだけに頼るべきではありません。

本記事のまとめ
| 対策 | 効果 | 難易度 |
|---|---|---|
| innerHTML → textContentへ置換 | 危険なシンクの排除 | 低(コード修正のみ) |
| DOMPurifyによるサニタイズ | HTML出力が必要な箇所の安全化 | 低(ライブラリ導入) |
| CSPの設定(nonce/hash) | スクリプト実行のブロック | 中(設定に知識が必要) |
| eval()禁止(Linterルール) | 危険なAPIの使用防止 | 低(ツール設定のみ) |
| 定期的なDOMXSS診断 | 潜在的問題箇所の早期発見 | 中(外部診断を推奨) |
DOM型XSSはWAFやサーバー側の検証をすり抜ける性質上、「アプリケーションコードそのもの」での対策が不可欠です。まず既存コードのinnerHTML使用箇所を洗い出し、textContentへの置換またはDOMPurifyの導入から着手することをお勧めします。
XSSやWebアプリケーション脆弱性の全体像については、OWASP Top 10とは?2021年版の全脆弱性リスト・各項目の意味と対策をわかりやすく解説もあわせて参照ください。
PR
体系的に学ぶ 安全なWebアプリケーションの作り方 第2版(徳丸浩/SBクリエイティブ)
XSSやSQLインジェクションをはじめとするWebアプリケーションの脆弱性を体系的に解説。DOM型XSSを含む攻撃手法と防御実装を実例とともに学べる、現場エンジニア必携の一冊です。
