聖域なきコードレビュー:React dangerouslySetInnerHTML が招く「DOMの地獄」と防衛の深淵
多くのエンジニアが、Reactの宣言的UIモデルを「安全な砂場」だと誤解している。確かにReactはデフォルトでXSS(Cross-Site Scripting)を防ぐ設計になっている。変数を埋め込めば自動的にエスケープされる。だが、ビジネス要件――CMSからのリッチテキスト出力や、レガシーなHTML資産の動的表示――という「実務の泥沼」に足を踏み入れた瞬間、その防御壁は dangerouslySetInnerHTML という名のパンドラの箱によって無効化される。
今回は、このプロパティが抱える脆弱性の本質と、単なる「サニタイズしましょう」という標語を超えた、アーキテクチャレベルでの防衛論を紐解く。
—
1. なぜ「dangerously」と冠されているのか:その低レイヤの脅威
dangerouslySetInnerHTML を使用するということは、Reactの仮想DOMによる差分計算とエスケープの恩恵を自ら放棄し、ブラウザのパーサーに直接HTMLの解釈を委ねることを意味する。
攻撃者の視点で見れば、これはDOMツリーの「構造的破壊」の入り口だ。例えば、ユーザー入力が適切にフィルタリングされていない状態で以下のコードが実行されたとする。
// 脆弱な実装例:悪意ある文字列がそのままDOMへ挿入される
ここで攻撃者が userProvidedContent に を注入すれば、onerror イベントハンドラが実行され、セッションCookieが流出する。これは基礎中の基礎だが、真の脅威はここからだ。DOM型XSSにおいては、サーバー側のログにすら残らないクライアントサイド完結型の攻撃が成立する。パケット構造を覗くWAF(Web Application Firewall)を完全にすり抜け、ブラウザのメモリ領域でJavaScriptの実行権を奪取される――これこそが、現代のフロントエンドセキュリティが直面している「可視性の欠如」である。
—
2. DOMPurifyによる防衛の「ガードレイル」設計
「HTMLをレンダリングせざるを得ない」状況において、我々が取るべき唯一の選択肢は、信頼できない文字列を「無害なツリー構造」へと物理的に変異させることだ。ここで登場するのが DOMPurify である。
単にライブラリを呼ぶだけでは不十分だ。セキュリティアーキテクトとして、このプロセスをパイプラインの不可分な一部として組み込む必要がある。
import DOMPurify from ‘dompurify’;
/
- セキュリティ強化されたレンダリングコンポーネント
- 外部ソースからのHTMLを安全な形式に「再構築」する
/
const SafeHtmlRenderer = ({ htmlContent }) => {
// 設定:サニタイズのポリシーを厳格に定義する
const sanitizedConfig = {
ALLOWED_TAGS: [‘b’, ‘i’, ‘em’, ‘strong’, ‘a’, ‘p’], // 許可リスト方式で最小権限を適用
ALLOWED_ATTR: [‘href’, ‘title’],
// プロトコル制限:javascript: 擬似スキームによるXSSを遮断
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|ftp):|[^:/?#](?:[/?#]|$))/i,
};
const cleanHtml = DOMPurify.sanitize(htmlContent, sanitizedConfig);
return (
);
};
なぜこの実装が重要か
1. ホワイトリスト方式の徹底: 許可されていないタグはすべて削除する。ブラックリスト方式は「攻撃者の未知の手法(ゼロデイ)」に対して無力であるため、採用してはならない。
2. プロトコルの検証: javascript: スキームを禁止することで、href 属性を介したコード実行を防ぐ。これは、現在のブラウザ仕様の脆弱性を突くペイロードに対する強力な防波堤となる。
—
3. 防御の「多重化」:CSP(Content Security Policy)という最後の砦
コードレベルのサニタイズは完璧ではない。ライブラリの脆弱性(CVE)や、DOMPurifyの誤設定というヒューマンエラーは必ず発生する。だからこそ、防御層を重ねる(Defense in Depth)必要がある。
ブラウザに送信するレスポンスヘッダとして、CSPを定義せよ。
Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-random123’; object-src ‘none’;
もし仮にサニタイズをすり抜けた悪意あるスクリプトが挿入されたとしても、script-src 'self' が設定されていれば、インラインスクリプトや外部の攻撃者サーバーからのスクリプト実行はブラウザによって即座にブロックされる。
—
4. 結びに:次世代のセキュリティアーキテクトへ
AIによるコード生成が普及し、攻撃者が「プロンプトインジェクション」で脆弱なコードを量産させる時代において、我々が守るべきは「コードの表面」ではなく「データの信頼性」そのものである。
dangerouslySetInnerHTML を使う際は、自分自身に対して常にこう問いかけてほしい。「このHTMLを、私は自分の手で一行ずつ解析し、悪意がないと断言できるか?」。答えがNoなら、DOMPurifyを噛ませ、CSPで蓋をし、さらに定期的な自動脆弱性スキャンをパイプラインに組み込むことだ。
セキュリティとは、終わりのない「追いかけっこ」ではない。堅牢なアーキテクチャという名の城壁を築き、攻撃者がわざわざ正面から攻め入るコストを極限まで高めることこそが、我々の仕事である。
技術の深淵に触れ、コードの裏側にある「意図」を読み解く力こそが、この激動のサイバー空間を生き抜く唯一の盾となるだろう。
コメント