DOMベースXSS:その「見えない汚染」をどう断ち切るか
現場でコードレビューをしていると、未だに「サーバーサイドでサニタイズしているから大丈夫」という甘い認識に出くわすことがある。だが、現代のWebアプリケーションにおいて、フロントエンドのJavaScriptが制御するDOM(Document Object Model)は、攻撃者にとって格好の戦場だ。
サーバーにリクエストが届かない「DOMベースXSS」は、WAFのログにも残らず、静的解析ツールをすり抜けることも多い。今日は、この泥沼のような脆弱性をどう特定し、完全に封じ込めるかについて、現場の知見を共有する。
—
1. 汚染経路(Taint Analysis)の解剖
DOMベースXSSの基本は、「汚染されたソース(Source)」から「危険なシンク(Sink)」へのデータの流れを追うことにある。
- Source(入口):
location.search,location.hash,document.referrer,window.nameなど。攻撃者がURLパラメータ等で操作可能なDOMプロパティだ。 - Sink(実行地点):
eval(),setTimeout(),innerHTML,outerHTML,document.write()など。これらに悪意ある文字列が渡されると、ブラウザはそれを「コード」として解釈してしまう。
攻撃シナリオ(PoC)
例えば、URLのクエリパラメータからユーザー名を取得して表示するだけの何気ないコードがあるとする。
// 脆弱な実装例
const params = new URLSearchParams(window.location.search);
const username = params.get("name");
document.getElementById("welcome").innerHTML = "ようこそ、" + username + "さん!";
攻撃者は、URLを細工して ?name=<img src=x onerror=alert(document.cookie)> と送るだけで、あなたのアプリケーション上で任意のスクリプトを実行できる。サーバーサイドのWAFは、このURLの#以降や、JavaScriptによる動的なDOM操作まで完全に監視できないことが多い。これが「見えない」と言われる所以だ。
—
2. 実践的な防御策:DOMPurifyによる「無害化」
脆弱性を見つけたら、まずは「入力値のバリデーション」を疑うはずだが、DOMベースXSSに対しては「出力直前のサニタイズ」こそが最も堅牢だ。
ここで最強の武器となるのが DOMPurify だ。正規表現でHTMLタグを置換しようなどという自作のロジックは、攻撃者にバイパスされるのがオチだ。DOMPurifyは、ブラウザのパース特性を熟知した上で、「許可されたタグと属性以外を徹底的に削ぎ落とす」というアプローチを取る。
セキュアな実装サンプル
ライブラリを導入し、innerHTMLへ代入する前に必ずサニタイズを通すこと。
// DOMPurifyを用いた安全な実装
import DOMPurify from 'dompurify';
const params = new URLSearchParams(window.location.search);
const username = params.get("name") || "ゲスト";
// 危険なシンクへ渡す前にサニタイズを実行
// 許可リストに基づき、悪意あるonerror属性などは自動的に削除される
const cleanUsername = DOMPurify.sanitize(username);
document.getElementById("welcome").innerHTML = "ようこそ、" + cleanUsername + "さん!";
—
3. 多層防御:CSP(Content Security Policy)の設定
コードレベルの修正だけでは、将来的な改修で再び脆弱性が混入するリスクがある。そこで、ブラウザの力を借りてインジェクションを物理的に封じる。
Nginx等のWebサーバー側で、以下のヘッダーを送信するように設定してほしい。
# Nginx設定例: CSPヘッダーの追加
# unsafe-inlineを排除し、信頼できるスクリプトのみを許可する
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';" always;
script-src 'self': 外部ドメインからのスクリプト読み込みを禁止。'unsafe-inline'を指定しない: インラインスクリプト(<script>alert(1)</script>)の実行をブラウザレベルで無効化する。これだけでDOMベースXSSの成功率は劇的に下がる。
—
後輩エンジニアへ贈る「教訓」
最後に一つだけ覚えておいてほしい。セキュリティは「一度設定して終わり」のチェックリストではない。
1. シンクを常に意識する: コードを書く際、innerHTMLやevalが目に入ったら「ここは汚染されているかもしれない」と反射的に警戒すること。
2. 型を信じる: TypeScriptを使っているなら、外部から来るデータには必ず unknown 型を割り当て、バリデーションを経由しない限り string として扱わせない設計が望ましい。
3. ブラウザの挙動を疑う: location.hash や window.name は、サーバーログに一切残らない。クライアントサイドの挙動こそが、現代の攻撃者の主戦場であることを忘れないでほしい。
コードを美しく保つことは、堅牢なシステムを作るための必須条件だ。脆弱性を放置することは、自分たちの手で鍵を壊して回るのと同じことだと肝に銘じておいてくれ。健闘を祈る。
コメント