【実務・中級編】DOMPurifyを用いたクライアントサイドサニタイズの実装 – アプリケーションセキュリティ & 安全な開発防御ガイド

XSSは「終わった脆弱性」ではない:DOMPurifyで制圧するクライアントサイドの死角

「XSS? 今どきフレームワークが自動でエスケープしてくれるでしょ?」

インシデント対応の現場で、若いエンジニアから何度この言葉を聞いたことか。確かに現代のReactやVue.jsはデフォルトでエスケープを効かせてくれる。だが、彼らが盲点にしているのは「動的なHTML生成」という禁断の果実だ。

CMSのプレビュー機能、リッチテキストエディタの出力、あるいは外部APIから受け取ったHTML文字列。これらをinnerHTMLやdangerouslySetInnerHTMLで画面にぶち込む時、君たちのアプリケーションは一瞬で「攻撃者の庭」と化す。

今日は、XSSの最終防衛線である「DOMPurify」を使った、妥協なき実装術を叩き込む。

—

1. なぜ「エスケープ」だけでは足りないのか

まず、攻撃者が狙うポイントを理解しろ。単純な反射型XSSはURLパラメータをいじるだけだが、格納型XSSが仕込まれたリッチテキストデータは、DBに保存された瞬間から時限爆弾になる。

攻撃のPoC(概念実証)例:
攻撃者は、一見無害な投稿フォームに以下のようなペイロードを混ぜ込む。

ブラウザがこのHTMLをパースする際、srcの読み込みに失敗し、onerrorイベントが発火する。これでセッションIDは外部に抜かれる。htmlspecialchars()のようなサーバーサイドのフィルタリングをすり抜けるエンコーディング手法は無数にある。だからこそ、「ブラウザにレンダリングさせる直前」で、DOM構造をサニタイズ(浄化)する必要があるんだ。

—

2. DOMPurifyによる鉄壁のホワイトリスト戦略

DOMPurifyは、ブラックリスト方式(悪意のあるタグを消す)ではなく、ホワイトリスト方式(許可されたタグ・属性以外はすべて消す)を採用している。これが最強たる理由だ。

実装サンプル:安全なHTMLレンダリング

まずはライブラリをインストールし、以下のように実装してくれ。

import DOMPurify from ‘dompurify’;

/

  • 外部から受け取った危険なHTMLを安全なDOMに変換する関数
  • @param {string} dirtyHtml – 信頼できないHTML文字列
  • @returns {string} – サニタイズ済みの安全なHTML文字列

/
function sanitizeHtml(dirtyHtml) {
return DOMPurify.sanitize(dirtyHtml, {
// 許可するタグと属性を厳格に指定
ALLOWED_TAGS: [‘p’, ‘b’, ‘i’, ‘em’, ‘strong’, ‘a’, ‘ul’, ‘ol’, ‘li’],
ALLOWED_ATTR: [‘href’, ‘title’, ‘target’],

// リンク先が javascript: などのスキームを含まないよう制限
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,

// 外部からのスクリプト注入を許さないための設定
RETURN_TRUSTED_TYPE: true // Trusted Types対応(ブラウザがサポートしていれば)
});
}

// 実際のDOMへの挿入例
const container = document.getElementById(‘content-area’);
const rawInput = apiResponse.htmlContent; // サーバーから取得したデータ

container.innerHTML = sanitizeHtml(rawInput);

—

3. 実務で「負けない」ための3つの鉄則

コードを書くだけがセキュリティじゃない。運用で泥沼にハマらないための知見を共有しておく。

① CSP(Content Security Policy)との多層防御

DOMPurifyをすり抜ける未知の脆弱性(DOMPurifyのバージョンアップ漏れなど)に備え、必ずヘッダーでCSPを設定しろ。

Nginx設定例:

信頼できないスクリプトの実行を禁止する
add_header Content-Security-Policy “default-src ‘self’; script-src ‘self’; object-src ‘none’; style-src ‘self’ ‘unsafe-inline’;”;

'unsafe-inline'は極力排除するのが筋だ。これが効いていれば、万が一DOMPurifyを突破されても、インラインスクリプトは発火しない。

② 入力値ではなく「出力先」を守る意識

開発者は「入力値のバリデーション」に必死になるが、コンテキストによって「安全な文字列」の定義は変わる。JavaScriptでHTMLを生成するなら、「DOMPurifyを通さないHTML挿入は原則禁止」というルールをチームのlint(eslint-plugin-securityなど)で強制せよ。

③ Trusted Typesの活用

最近のブラウザが実装している「Trusted Types」は、innerHTMLへの直接代入をブラウザレベルで禁止できる。DOMPurifyと組み合わせることで、意図しないHTML注入を完全に遮断できる最強の盾になる。

—

最後に:セキュリティは「性悪説」で設計しろ

「このデータは社内ツールから来るから大丈夫だろう」という甘えが、数々の大規模情報漏洩の引き金になってきた。

DOMPurifyは、いわば「玄関に置く高性能な金属探知機」だ。これを適切に設定し、CSPという「警備員」を配置し、さらにTrusted Typesという「ロック」をかける。この三段構えがあって初めて、エンジニアは胸を張ってプロダクトを世に出せるんだ。

今日から君たちのプロジェクトで、innerHTMLを検索してくれ。もしそこにDOMPurifyが見当たらなければ、それが君たちの「次なる戦場」だ。健闘を祈る。

コメント

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