DOMベースXSSの「真の恐怖」:サーバーログに残らない静かなる侵入者
現場でインシデント対応をしていると、「WAFも入れているし、サーバーサイドのバリデーションも完璧だ。なぜ情報漏洩が起きた?」と頭を抱えるエンジニアによく遭遇する。その答えの多くは、「サーバーサイドには一切触れないDOMベースXSS」という盲点にある。
今日は、現代のWebアプリケーション開発において「見えない脅威」の筆頭であるDOMベースXSSについて、本質的なメカニズムと、明日から使える防御術を叩き込む。
—
1. なぜDOMベースXSSは「サーバーのログ」をすり抜けるのか
従来の反射型(Reflected)XSSや蓄積型(Stored)XSSは、サーバーサイドが不正なスクリプトを含んだリクエストを処理し、レスポンスに含めることで発生する。つまり、WAFやサーバーのアクセスログで検知が可能だ。
しかし、DOMベースXSSは完結の場が「ブラウザのメモリ内」にある。
攻撃者はURLのフラグメント(#以降)や、JavaScriptで操作可能なwindow.nameなどを利用して、サーバーにペイロードを送信することなくブラウザ側でスクリプトを実行させる。サーバー側から見れば、ただの「正常なページ読み込み」にしか見えない。これが、この攻撃が恐ろしい理由だ。
典型的な「危険なフロー」
1. Source(起点): location.hash や location.search など、ユーザーが操作可能なデータ。
2. Sink(終点): innerHTML, outerHTML, document.write(), あるいは eval() など、文字列をHTMLとして解釈したり実行したりする関数。
「URLのパラメータを読み取って、特定のDOM要素に表示する」という何気ない機能が、Sinkに直結した瞬間に爆弾へと変わる。
—
2. 攻撃者が狙う「Sink」のPoC
例えば、以下のような何の変哲もないJavaScriptコードがプロジェクト内に放置されていないだろうか?
// 悪い例:URLフラグメントから名前を取得して表示するコード
const hash = decodeURIComponent(window.location.hash.substring(1));
const userGreeting = document.getElementById(‘greeting’);
userGreeting.innerHTML = “ようこそ、” + hash + “さん!”;
攻撃者はこれに対し、以下のURLを送りつけるだけでいい。
https://example.com/#
このURLを踏んだ瞬間、innerHTMLは悪意のあるHTMLをパースし、onerrorイベントが発火する。サーバーには#以降の情報は送信されないため、WAFは沈黙する。これが「痕跡を残さない侵入」の正体だ。
—
3. 実務で勝つための防御実装:セキュアなコードへの書き換え
脆弱性を防ぐための黄金律は、「危険なSink(innerHTML等)を使わず、安全なAPIを使うこと」に尽きる。
修正案1:textContentを利用する(推奨)
HTMLとしてパースさせる必要がないなら、テキストとしてのみ扱うtextContentを使うのが鉄則だ。これだけでXSSは物理的に不可能になる。
// 良い例:textContentで安全に表示する
const hash = decodeURIComponent(window.location.hash.substring(1));
const userGreeting = document.getElementById(‘greeting’);
// HTMLタグはただの文字列として扱われるため、スクリプトは実行されない
userGreeting.textContent = “ようこそ、” + hash + “さん!”;
修正案2:信頼できないデータをHTMLに含める場合(DOMPurify)
どうしてもHTMLをレンダリングする必要がある場合は、必ずライブラリを通して「無害化(サニタイズ)」すること。自前で正規表現を書いてホワイトリストを作るのは、セキュリティの専門家でも避けるべき悪手だ。
// npm install dompurify
import DOMPurify from ‘dompurify’;
const dirtyHTML = getUrlParam(); // ユーザー入力
const cleanHTML = DOMPurify.sanitize(dirtyHTML); // 無害化
document.getElementById(‘content’).innerHTML = cleanHTML;
—
4. 多層防御としての「CSP」設定
コードレベルの修正に加え、ブラウザ側で不正なスクリプトの実行を強制的に遮断するContent Security Policy (CSP)をHTTPヘッダーで設定しておくべきだ。
nginxの設定例:
信頼できるソース以外からのスクリプト実行を禁止する
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’; frame-ancestors ‘none’;”;
default-src 'self': 自ドメイン以外の読み込みを禁止。script-src 'self': インラインスクリプトやeval()の使用を原則禁止。これにより、DOMベースXSSが仕込まれても実行がブロックされる。
—
セキュリティチーフからの「現場の心得」
DOMベースXSSの恐ろしさは、開発者が「サーバーにデータを送っていないから安全だ」と錯覚してしまう点にある。
- 「動的にDOMを書き換えるコード」を見たら、そこがSinkだと疑え。
innerHTMLを見たら「バグ」だと思え。- 開発の初期段階からCSPを厳格に適用し、違反レポートを監視せよ。
セキュリティは「魔法の杖」ではなく「日々の規律」だ。コードを書く際、常に「このデータは信頼できるか?」「この関数はHTMLを解釈してしまうか?」という問いかけを忘れないこと。それが、君のアプリケーションを鉄壁にする唯一の道である。
さあ、今すぐプロジェクトのコードを innerHTML で検索して、一つずつ安全な実装に置き換えていくとしよう。それが、プロのエンジニアの仕事だ。
コメント