【実務・中級編】DOMベースXSSのクライアントサイド脆弱性とJavaScript実行フロー – アプリケーションセキュリティ & 安全な開発防御ガイド

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 で検索して、一つずつ安全な実装に置き換えていくとしよう。それが、プロのエンジニアの仕事だ。

コメント

タイトルとURLをコピーしました