フロントエンドの「死角」を突く:DOMベースXSSの正体と、現場で通用する防御術
多くのエンジニアが「XSS対策はサーバーサイドでエスケープすれば万全」という幻想を抱いている。だが、残念ながらそれは現代のWebアプリケーションにおいては半分しか正解ではない。
サーバーを一切経由せず、ブラウザの中で完結する「DOMベースXSS」は、WAFをすり抜け、ログにも残りにくい。今回は、この攻撃のメカニズムを解剖し、今日からコードベースに組み込める現実的な防御策を叩き込む。
—
1. なぜ「サーバーサイドの対策」だけでは不十分なのか
従来のXSSは、サーバーが生成したHTMLに悪意あるスクリプトが混入する「反射型」や「蓄積型」だった。しかし、モダンなSPAや動的なUIを持つサイトでは、JavaScriptがブラウザ上のデータを直接読み込み、そのまま実行するケースが増えている。
これがDOMベースXSSの主戦場だ。サーバーは一切関与しない。攻撃者はURLのフラグメント(#以降)やクエリパラメータを細工し、クライアントサイドのコードがそれを「安全な入力」だと誤認してDOMを操作するのを待つだけだ。
攻撃のメカニズム:ソースとシンクの「汚染」
この脆弱性は常に「ソース」と「シンク」の組み合わせで発生する。
- ソース (Source): 外部から制御可能なデータ源。
location.hash,location.search,document.referrerなど。 - シンク (Sink): データが実行・レンダリングされる危険なポイント。
innerHTML,outerHTML,document.write(),eval(),setTimeout(string)など。
例えば、location.hashから取得した文字列をそのままinnerHTMLに流し込めば、それがどんな悪意あるタグであってもブラウザは忠実に実行してしまう。
—
2. 実践:攻撃者が狙う「PoC(概念実証)」の罠
もし君が担当するアプリの検索結果画面に、以下のようなJavaScriptがあったら即刻修正が必要だ。
// 脆弱な実装例:URLのハッシュをそのままHTMLに書き出す
const hash = window.location.hash.substring(1);
document.getElementById(‘search-result’).innerHTML = “検索キーワード: ” + hash;
攻撃者は、以下のURLをユーザーに踏ませるだけでセッションハイジャックが可能になる。
https://example.com/search#
このコードはサーバーログに一切残らない。WAFも「ハッシュフラグメントはサーバーに送信されない」という仕様ゆえに無力だ。
—
3. 「コピペで防ぐ」セキュアな実装パターン
対策の鉄則は一つ。「外部入力を信頼せず、DOMに触れる際は型を強制する」ことだ。
推奨:textContent を使う(もっとも簡単な修正)
innerHTMLはHTMLとして解析されるが、textContentは純粋なテキストとして扱われる。これだけでXSSは無効化できる。
// 安全な実装例
const hash = window.location.hash.substring(1);
const resultElement = document.getElementById(‘search-result’);
// innerHTMLではなくtextContentを使うことで、タグが文字列としてエスケープされる
resultElement.textContent = “検索キーワード: ” + hash;
さらに堅牢にする:DOMPurifyの導入
もしどうしてもHTMLのレンダリングが必要な場合(リッチテキストエディタ等)、自前のエスケープ関数を書くのはやめろ。必ず実績のあるライブラリを使え。
// npm install dompurify
import DOMPurify from ‘dompurify’;
const dirtyHash = window.location.hash.substring(1);
// 許可されたタグ以外を徹底的に除去する
const cleanHTML = DOMPurify.sanitize(dirtyHash);
document.getElementById(‘search-result’).innerHTML = cleanHTML;
—
4. インフラ・ブラウザ層からの「多層防御」
アプリ側の修正と並行して、万が一のバグ混入に備えた「最後の砦」を築く必要がある。
Content Security Policy (CSP) の設定
NginxなどのレスポンスヘッダーでCSPを設定し、インラインスクリプトの実行を禁止せよ。
nginx.conf 設定例:
インラインスクリプトを禁止し、信頼されたソースのみ許可する
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’ https://trusted.cdn.com; object-src ‘none’;”;
この設定があれば、仮に攻撃者がonerrorを挿入しても、ブラウザ側で実行がブロックされる。
—
最後に:現場のエンジニアへ
セキュリティは「完璧なコード」を書くことではなく、「いつか必ず綻びが出る」という前提でシステムを設計することだ。
1. innerHTML は禁忌。 使うなら textContent か、DOMPurify を通した後のものに限る。
2. ソースを疑え。 location系、document.referrer系を扱う際は、必ずバリデーションを通す。
3. CSPで守りを固める。 アプリのバグをインフラで防ぐ姿勢が、君のサービスをインシデントから救う。
この知識をチームに共有し、明日のデプロイメントで一つでも多くの脆弱性を潰してくれ。健闘を祈る。
コメント