XSS防御の終焉と再生:DOMPurifyを「ただのライブラリ」で終わらせないための設計思想
Webアプリケーションの脆弱性診断において、XSS(クロスサイトスクリプティング)はもはや「古典」として片付けられがちだ。しかし、現場の最前線に立つ我々にとって、XSSは依然として最も狡猾なエントリポイントであり続けている。特に、現代のフロントエンドフレームワークや、LLMが生成したコンテンツをそのままブラウザにレンダリングするアプリケーションにおいて、DOMPurifyのようなサニタイズライブラリは、単なる「便利な道具」ではなく、アプリケーションの生存を左右する最後の防衛ラインとなる。
今日は、巷に溢れる「ライブラリを入れれば安全」という甘い神話を破壊し、アーキテクトとして守るべき防御の深淵について語ろう。
—
1. なぜライブラリ選定で「DOMPurify」一択なのか?
多くの開発者がライブラリを選定する際、GitHubのスター数や更新頻度だけを見る。しかし、我々が重視するのは「ブラウザのパーサー挙動をどこまで正確に模倣し、かつ無効化できているか」という点だ。
XSSの根本原因は、HTMLパーサーの曖昧さと、仕様(HTML5 Spec)の複雑さにある。攻撃者は常に、「ブラウザが解釈できるが、WAFや不完全なフィルタが無視する文字列」を突きつけてくる。
DOMPurifyが選ばれる理由は、単なるフィルタリング機能ではない。ブラウザのDOMツリーを構築する前段階で、攻撃者が潜ませる「非標準的なタグや属性」を、現行ブラウザの挙動に合わせて再帰的にサニタイズするその厳格さにある。
2. ホワイトリスト設計の勘所:デフォルト設定は「甘い」
DOMPurifyをデフォルトのまま使うのは、強固な金庫に鍵をかけずに警備員を立たせているようなものだ。セキュリティアーキテクトとして、以下の設定をベースラインとすべきだ。
import DOMPurify from ‘dompurify’;
/
- プロダクション環境における厳格なサニタイズ設定
/
const clean = DOMPurify.sanitize(dirtyInput, {
// 1. 許可するタグを最小限に絞る(Allowlist)
ALLOWED_TAGS: [‘b’, ‘i’, ‘em’, ‘strong’, ‘a’, ‘p’, ‘br’],
// 2. 許可する属性も厳格に定義
ALLOWED_ATTR: [‘href’, ‘title’, ‘target’],
// 3. プロトコル制限を徹底(javascript:スキームを許さない)
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|ftp):|[^:/?#](?:[/?#]|$))/i,
// 4. mXSS対策:DOMPurifyが内部でブラウザのバグを回避するモードを有効化
RETURN_DOM: false,
SANITIZE_DOM: true,
// 5. 悪意のあるデータ属性を排除
ADD_ATTR: [‘target’],
FORBID_ATTR: [‘style’, ‘onerror’, ‘onload’], // 特に重要
});
なぜ FORBID_ATTR が必要なのか?
現代のブラウザは非常に賢いが、その分「隠し機能」も多い。style 属性を許可すれば、expression() や url() を使ったCSSインジェクションでJSを実行させる手法(古いブラウザや特定環境)が存在する。また、最近ではプロトコルハンドラを悪用した攻撃も巧妙化している。デフォルトのブラックリストに依存せず、常に「ホワイトリスト(許可するもの)以外は徹底的に排除する」という哲学を貫くこと。
3. 次世代の脅威:LLMプロンプトインジェクションとDOMPurifyの役割
今、セキュリティの最前線で起きているのは、ユーザー入力だけでなく「LLMが生成したコンテンツ」のサニタイズだ。生成AIは、プロンプトインジェクションにより、意図せずタグや難読化されたデータURIを生成することがある。
これを防ぐには、「出力側でのサニタイズ」だけでなく、「ガードレイル」による多層防御が必須となる。
- ガードレイル層: LLMの出力トークンに対して、Regexや別のパーサーを通し、構造的に異常なHTMLが生成されていないか確認する。
- DOMPurify層: 最終的にレンダリングする直前で、DOMPurifyが「ブラウザが解釈可能なコード」として成立しないように無害化する。
この二段構えこそが、AI時代におけるXSS対策のアーキテクチャだ。
4. チーフホワイトハッカーからの提言:監査と自動化
どれほど優れたライブラリを使っても、コードを修正してDOMPurify.sanitizeを削除してしまえば全てが無に帰す。以下の観点をCI/CDパイプラインに組み込むことを推奨する。
1. 静的解析(SAST)の強化: Semgrep等のツールを使い、dangerouslySetInnerHTML(React)や v-html(Vue)が使用されている箇所を、DOMPurifyのラッパー関数を通していない場合にCIを落とすルールを定義せよ。
2. CSP(Content Security Policy)との併用: DOMPurifyは「最後の砦」だが、JSの実行自体を制限するCSP(特に script-src 'self')がなければ、万が一の漏れが即座に被害に直結する。ライブラリとヘッダー設定は、車の両輪だ。
3. 継続的なFuzzing: パッチが当たるたびに、DOMPurify自体の挙動を疑うわけではないが、アプリケーション固有の入力を想定したFuzzingテストを怠るな。特に、難読化されたBase64エンコードデータや、Unicode文字の混入によるパーサーの「解釈のズレ」を狙う攻撃は、手動テストでは見抜けない。
結びに:技術は手段、目的は「信頼」
セキュリティとは、技術の積み重ねであると同時に、心理戦でもある。攻撃者は「開発者がここをサボるだろう」という心の隙を突く。DOMPurifyの設定一つとっても、なぜその属性を許可し、なぜそのタグを拒絶するのか。そのロジックを言語化し、チーム全体で共有できているか。
「ライブラリを入れたから安全」と言えるのは、そのライブラリの内部挙動を理解し、ブラウザの仕様と攻撃手法の狭間にある危うさを自覚している人間だけだ。
コードを書くとき、常に問い続けてほしい。「この文字列がブラウザのエンジンを通ったとき、俺が意図しないコードとして実行される可能性は0.01%もないか?」と。
その疑い深さこそが、我々エンジニアが持つべき最も強力なセキュリティツールなのだ。
コメント