現場の最前線で語る:HTML無害化の「泥沼」とDOMPurifyによる絶対防衛ライン
現場でコードをレビューしていると、いまだに「strip_tags を使えば安全」と信じ込んでいる設計に出くわすことがある。正直に言おう。それは、鍵の壊れた玄関に「入るな」と張り紙をしているようなものだ。
Webアプリケーションにおいて、ユーザー入力をHTMLとして許容するという行為は、火遊びに近い。だが、リッチテキストエディタなどが標準装備の現代において、それを禁止するのは現実的ではない。だからこそ、「何を許可するか」を厳格に定義するホワイトリスト方式の無害化が唯一の正解となる。
今日は、なぜ中途半端なサニタイズが命取りになるのか、そしてDOMPurifyを用いた「鉄壁の防御」をどう実装すべきか、実戦的な視点で叩き込む。
—
なぜ「ブラックリスト方式」では防げないのか?
多くのエンジニアが陥る罠が、「危険なタグを消す(ブラックリスト)」というアプローチだ。例えば、タグを除去したとしても、攻撃者は別のルートを突く。
悪夢のPoC:難読化と属性注入
攻撃者は、あなたが想像もしないような構文でブラウザを騙す。
strip_tags はタグを消すだけだが、onerror や onload といったイベントハンドラまでは考慮しない。さらに、エンコードされた文字や、ブラウザのパースエラーを誘発するような「不正なHTML構造」を送り込まれると、防御ロジックそのものがバイパスされる。手作りの正規表現やブラックリストによるサニタイズは、必ずどこかで綻びる。
---
DOMPurifyによる「ホワイトリスト」防衛の実装
現在、フロントエンド開発において最も信頼できるのが DOMPurify だ。これは、DOMの構造を解析し、許可されたタグと属性のみを厳選して抽出する。ブラックリストではなく、「許可されたもの以外はすべて捨て去る」という潔い設計だ。
JavaScript (フロントエンド/Node.js) での実装例
まずは、プロジェクトにインストールする。
npm install dompurify jsdom
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
// JSDOMを使って環境を作る(Node.js環境の場合)
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
/
- 安全なHTMLに変換する関数
- @param {string} dirtyInput - ユーザーからの未検証入力
/
function sanitizeHTML(dirtyInput) {
// 許可するタグと属性を厳格に指定する
const config = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'title'],
// hrefがjavascript:プロトコルを含まないように制限
ALLOWED_URI_REGEXP: /^(?:(?:f|ht)tps?|mailto|tel):/i
};
return DOMPurify.sanitize(dirtyInput, config);
}
// 使用例
const userInput = 'Hello';
const cleanHtml = sanitizeHTML(userInput);
console.log(cleanHtml); // 出力: Hello (imgタグは問答無用で消える)
なぜこれが最強なのか?
1. DOM解析ベース: 文字列置換ではないため、パースエラーを狙った攻撃が効かない。
2. プロトコル制御: javascript: スキームの実行をデフォルトで防ぐ。
3. 継続的な保守: ゼロデイに近いブラウザの挙動変化にも、ライブラリの更新で追従できる。
---
バックエンドでの多重防衛(Defense in Depth)
DOMPurifyで無害化しても、「クライアント側だけで完結させない」のがセキュリティの鉄則だ。API経由で直接データベースに不正な文字列が注入されることを防ぐ必要がある。
もしPHPでサーバーサイドレンダリングを行っているなら、HTML Purifier を採用するべきだ。
// PHP用 HTML Purifier の利用例
require_once 'library/HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
// 許可するHTML要素をホワイトリスト設定
$config->set('HTML.Allowed', 'b,i,strong,em,a[href]');
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($user_input);
---
運用上の「盲点」:WAFとCSPの二段構え
最後に、コード以外の防衛線についても触れておく。
- WAF (Web Application Firewall):
AWS WAFやCloudflareを利用しているなら、「SQLインジェクション」や「XSS」のシグネチャによるブロックルールを必ず有効にすること。これらは「既知の攻撃パターン」を入り口で弾くための防波堤となる。
- CSP (Content Security Policy):
万が一、サニタイズをすり抜けて不正なスクリプトが埋め込まれても、CSPを設定していればブラウザ側で実行を阻止できる。
Nginx設定例 (CSPヘッダーの追加):
信頼できないインラインスクリプトの実行を禁止する
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';";
---
チーフエンジニアからのアドバイス
セキュリティは「完成」しない。開発者が一番やってはいけないのは、「ライブラリを入れたからもう大丈夫だ」と油断することだ。
ライブラリのバージョンは常に最新に保つこと。そして、設計段階で「そもそも、この機能にHTMLの入力は本当に必要なのか?」と自問自答してほしい。もしMarkdownで代用できるなら、Markdownを採用する。「不要なものは実装しない」ことこそが、最強のセキュリティ対策だ。
泥臭い作業かもしれないが、一つひとつの入力を疑い、堅牢なホワイトリストで囲い込む。それが、君が守るべきシステムと、エンドユーザーの信頼を繋ぎ止める唯一の手段だ。現場からは以上だ。
コメント