DOM-based XSSの正体:サーバーログに痕跡を残さない「見えない凶器」
現場でインシデント対応をしていると、サーバー側のWAFログが真っ白なのに、特定のユーザーから「アカウントが乗っ取られた」「勝手に決済画面に飛ばされる」といった悲鳴が届くことがあります。これがDOM-based XSSの恐ろしいところです。
一般的なXSSはサーバーが生成したHTMLに攻撃コードが埋め込まれますが、DOM-based XSSは「サーバーは潔白」です。攻撃はブラウザ内部のJavaScriptだけで完結します。サーバーのログには一切痕跡が残らないため、多くのエンジニアが「何が起きたのか」を突き止めるのに苦労します。
1. なぜDOM-based XSSは「盲点」なのか
攻撃者は、URLのフラグメント識別子(#以降)を悪用します。この部分はブラウザからサーバーへ送信されないため、どんなに強力なWAFをゲートに置いても防御できません。
攻撃のPoC(概念実証):
例えば、こんな脆弱なコードがフロントエンドにあるとします。
// URLのハッシュ値を読み取って、そのまま画面に表示するだけの危険なコード
const hash = window.location.hash.substring(1);
document.getElementById(‘welcome-msg’).innerHTML = “ようこそ、” + decodeURIComponent(hash) + “さん!”;
攻撃者が以下のURLをターゲットに踏ませた瞬間、ゲームオーバーです。
https://example.com/#
innerHTMLはブラウザに対し、中身を「文字列」ではなく「HTML要素」として解釈させます。その結果、悪意あるスクリプトが即座に実行され、セッションCookieが外部の攻撃者サーバーへ送信されます。
2. 泥臭い現場の防御策:DOM-based XSSを無力化する
この脆弱性を叩き潰すには、「外部からの入力を、信頼できないHTMLとして処理させない」という原則を徹底するしかありません。
対策A:危険なシンク(Sink)を避ける
innerHTMLやouterHTML、document.write()などの「文字列をHTMLとして解釈する関数」は、可能な限り避けてください。代わりとしてtextContentやinnerTextを使うのが現代の鉄則です。これらは入力値を「ただのテキスト」として扱うため、スクリプトが注入されても単なる文字列として画面に表示されるだけで、実行はされません。
対策B:実務で使えるセキュアな実装パターン
以下に、安全な書き換え例を示します。
// 【セキュアな実装例】
const hash = window.location.hash.substring(1);
const welcomeElement = document.getElementById(‘welcome-msg’);
// innerHTMLではなく、textContentを使用してDOMを操作する
// これにより、ハッシュ値にスクリプトが含まれていても「無害な文字列」として表示される
welcomeElement.textContent = “ようこそ、” + decodeURIComponent(hash) + “さん!”;
3. 多層防御としてのCSP(Content Security Policy)
コードレベルの修正に加え、ブラウザ側で「怪しいスクリプトをそもそも実行させない」ための保険をかけます。NginxなどのWebサーバー設定でCSPを付与しましょう。
Nginx設定例:
HTTPレスポンスヘッダーにCSPを追加
‘unsafe-inline’ を排除し、信頼できるソースからのスクリプトのみを許可する
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’;”;
この設定を入れておけば、万が一コードの修正漏れがあったとしても、外部ドメインからの怪しいスクリプト実行や、インラインスクリプトの実行がブロックされます。
セキュリティチーフからのアドバイス
DOM-based XSSを防ぐために、私が若手エンジニアによく伝えているのは「ユーザー入力は常に毒だと思え」という言葉です。
特にSPA(Single Page Application)が主流の今、URLのパラメータやハッシュ、さらにはlocalStorageやsessionStorageから読み込んだデータでさえも、フロントエンドで処理する際は「出力時エンコード」や「テキストノードへの直接代入」を徹底してください。
サーバーサイドの防御が強固になっても、フロントエンドがガバガバでは、ユーザーを守ることはできません。まずはプロジェクトのソースコードで innerHTML を検索することから始めてみてください。それがあなたのアプリケーションを、より強固な要塞へと変える第一歩です。
コメント