XSSの「教科書」はもう捨てろ:現場で通用するDOMPurify防御の深層
「サニタイズしてますから大丈夫です」。
コードレビューでこの言葉を聞くたび、私は背筋が凍る思いがする。htmlspecialchars() を適当に使って「はい、XSS対策完了」と満足しているエンジニアが多すぎるからだ。
いいか、XSS(クロスサイトスクリプティング)は単なる「警告ダイアログが出る脆弱性」じゃない。セッションハイジャック、なりすましによる権限昇格、そしてバックエンドの管理画面を操作される「ビジネスロジック崩壊」の入り口だ。
今日は、現代のフロントエンド開発において「HTMLをレンダリングする」という危険な行為を、どうやって安全に完遂させるか。その実戦的な勘所を叩き込む。
—
1. なぜ「自作のサニタイズ」が死を招くのか
よくある失敗例が、正規表現で タグを削除しようとするコードだ。
// 悪い例:絶対に真似するな
const safeInput = userInput.replace(/
攻撃者は笑うよ。 と入れ子にしたり、onerror 属性を仕込んだり、あるいはエンコードを二重にしたりと、回避手法は無限にある。ブラックリスト方式は、攻撃者の想像力に勝てない。
我々が取るべき唯一の正解は、「DOMPurify」のような、ブラウザのパースエンジンを模倣したホワイトリストベースのライブラリを使うことだ。
---
2. 実戦:DOMPurifyによる防御実装(JavaScript)
フロントエンドで信頼できないHTMLを扱う場合、DOMPurify一択だ。これは単なる文字列置換ではなく、安全なDOMツリーを構築し直す「最強の門番」だ。
推奨される実装コード
import DOMPurify from 'dompurify';
/
- ユーザー入力を安全にHTMLとしてレンダリングする関数
- @param {string} dirtyInput - 信頼できないHTML文字列
/
function renderSecurely(dirtyInput) {
// 1. 基本設定:許可するタグと属性を厳格にホワイトリスト化
const config = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title', 'target'],
// 外部へのリンクは必ず noopener noreferrer を強制する
ADD_ATTR: ['target'],
FORBID_TAGS: ['style', 'script', 'iframe', 'object'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'style']
};
// 2. サニタイズ実行
const cleanHtml = DOMPurify.sanitize(dirtyInput, config);
// 3. 安全なHTMLを挿入
document.getElementById('content-area').innerHTML = cleanHtml;
}
ここがプロのこだわり
FORBID_ATTRの明示: ホワイトリスト方式であっても、攻撃の入り口になりやすいon系のイベントハンドラは明示的に禁止する。FORBID_TAGS: CSSインジェクションを狙うstyleタグや、フィッシングの踏み台になるiframeは、用途がない限り徹底して排除する。
---
3. インフラ側で叩き込む「最後の防衛線」
アプリ側でどれだけ頑張っても、ゼロデイや設定ミスはある。そこで、ブラウザの挙動を制限する「Content Security Policy (CSP)」が不可欠だ。
Nginxで以下のレスポンスヘッダーを付与し、たとえXSSが成功しても「外部スクリプトの実行」を阻止せよ。
Nginx設定ファイル例
unsafe-inlineを禁止し、スクリプトの実行元を厳格に制限する
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted.cdn.com; object-src 'none'; style-src 'self' 'unsafe-inline'; base-uri 'self';";
script-src 'self': 外部の怪しいJSソースの読み込みを遮断する。これが効いていれば、攻撃者の外部サーバーへCookieを送信する攻撃は9割防げる。
---
4. セキュリティチーフからの「最後の警告」
実戦では、「HTMLそのものを保存しない」のが最強のセキュリティだ。Markdownで保存し、フロントエンドで安全なパーサーを通してHTMLに変換する。これが、私がこれまで見てきた中で最も堅牢なシステムだ。
最後に一つだけ覚えておいてほしい。
「セキュリティは、機能の一部であって、後付けのオプションではない」。
コードを書くとき、「もしこれが悪意あるユーザーの入力だったら?」と常に問いかけろ。その一瞬の疑念が、明日の大規模な個人情報流出事故を防ぐんだ。
もし現場で「この実装で本当に大丈夫か?」と悩む夜があったら、また戻ってこい。技術は進化するが、攻撃者の執念は変わらない。我々エンジニアは、その先を行き続けなければならないのだから。
コメント