XSSは「終わった脆弱性」か?――DOMベースXSSが突きつける現代の脅威
「XSS対策? htmlspecialchars を通せば終わりでしょ」。
もし君のチームでそんな会話が聞こえてきたら、そのプロジェクトは既に危うい。確かに反射型や蓄積型XSSは、サーバーサイドでエスケープを徹底すれば防げる。だが、現代のWebアプリケーションは「クライアントサイド」で動く巨大なロジックの塊だ。
今日は、教科書的な説明はすっ飛ばして、現場で本当に恐ろしい「DOMベースXSS」の深淵に切り込む。インフラからフロントまでを統括する立場として、君たちが明日から書くコードに何をすべきか、泥臭い現実を語ろう。
—
1. XSSの「今」を知る:3つの分類と落とし穴
まず前提を整理する。XSSは大きく3つに分類されるが、攻撃者が狙うポイントは明確に異なる。
- 反射型 (Reflected XSS): ユーザーの入力をURLパラメータ等から受け取り、即座に画面へ出力する。サーバーサイドのレスポンスに直接悪意あるスクリプトが混入する。
- 蓄積型 (Stored XSS): データベースに悪意あるスクリプトを保存させる。掲示板やプロフィール欄が標的。全ユーザーが被害に遭うため、影響範囲が最も広い。
- DOMベースXSS: これが今回の主役だ。「サーバーのレスポンスは関係ない」。JavaScriptがURLのフラグメント(
#以降)やローカルストレージからデータを読み取り、そのままinnerHTML等の「シンク(危険な実行関数)」に流し込むことで発生する。
なぜDOMベースが厄介なのか?
サーバーログには何も残らない。WAF(Web Application Firewall)をすり抜ける。なぜなら、悪意あるコードはサーバーを経由せず、クライアントのブラウザ内で完結するからだ。
—
2. DOMベースXSSの「PoC」:なぜJavaScriptは騙されるのか
攻撃者は、開発者が「サーバーに送らないデータなら安全だろう」と油断している隙を突く。例えば、こんな脆弱なコードを君は書いていないか?
// 脆弱な例:URLのパラメータをそのまま表示するコード
const params = new URLSearchParams(window.location.search);
const username = params.get(“name”);
// ここでinnerHTMLに直接流し込むのが”シンク”(危険な接点)
document.getElementById(“welcome-msg”).innerHTML = “ようこそ、” + username + “さん!”;
もし攻撃者が ?name= というURLを送りつければ、ブラウザは即座にそのスクリプトを実行する。開発者が必死にサーバーサイドでエスケープ処理を書いていても、このコードには何の影響も与えないんだ。
—
3. 完全防御のためのセキュア実装術
対策の鉄則は「信頼できない入力値を直接シンクに渡さない」こと、そして「実行コンテキストを制御する」ことだ。
① JavaScriptでのセキュアな実装(修正例)
innerHTML は禁じ手だ。テキストを表示するだけなら textContent を使う。これだけで、ブラウザはタグをパースせず、純粋な文字列として扱うようになる。
// 修正後:textContentを使用する
const params = new URLSearchParams(window.location.search);
const username = params.get(“name”);
// textContentなら、悪意あるタグもただの文字として表示される(安全)
document.getElementById(“welcome-msg”).textContent = “ようこそ、” + username + “さん!”;
② CSP(Content Security Policy)によるインフラレベルの防御
コードの修正が追いつかないレガシーな箇所がある場合でも、CSPがあれば「防波堤」を作れる。HTTPレスポンスヘッダーに以下を設定せよ。
Nginx設定例:インラインスクリプトの実行を禁止し、攻撃を防ぐ
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’;”;
script-src 'self':信頼できるソースからのスクリプトのみ実行を許可。object-src 'none':プラグイン経由の攻撃を遮断。
—
4. プロの現場で生き残るための「思考の癖」
セキュリティは「ツールを入れたら終わり」ではない。以下の3点をチームの文化として根付かせてほしい。
1. 「危険なシンク」を覚える: innerHTML, outerHTML, document.write(), eval(), setTimeout(string)。これらを見つけたら、「なぜこれが必要なんだ?」と一度立ち止まること。
2. DOMPurifyの導入: どうしてもHTMLをレンダリングする必要がある場合は、自作のフィルタリングなど考えず、[DOMPurify](https://github.com/cure53/dompurify) のような枯れたライブラリを使い、サニタイズ(無害化)を徹底すること。
3. WAFは「最後の砦」と心得る: WAFは重要だが、DOMベースXSSのようなクライアントサイド攻撃には無力な場合が多い。アプリケーション側のロジックで防御するのが、真のエンジニアの流儀だ。
最後に
セキュリティの戦いに「完全な正解」はない。しかし、脆弱性を一つずつ消し込み、堅牢な設計を積み重ねることはできる。今日話したDOMベースXSSの仕組みを理解すれば、君たちの書くJavaScriptは確実に一段上のレベルに達するはずだ。
コードをレビューする時、innerHTML を見つけたら、この記事を思い出してくれ。それが、システムを守るための第一歩だ。健闘を祈る。
コメント