Reactの「自動エスケープ」という甘い罠:dangerouslySetInnerHTMLが引き起こす脆弱性の深層
Reactを愛するエンジニア諸君なら、一度は「Reactは安全だ」という言説を耳にしたことがあるだろう。確かに、JSX内で変数を展開する際、Reactはデフォルトで文字列をエスケープしてくれる。
{userInput}
と書けば、userInput に タグが含まれていても、それは単なる文字としてレンダリングされる。これがXSS(Cross-Site Scripting)に対する「一次防衛線」であることは間違いない。
しかし、この自動エスケープという「安全装置」に全幅の信頼を置くのは、セキュリティの専門家としてはあまりにナイーブだ。今日は、Reactが提供するこの機能のメカニズムを解剖し、なぜ dangerouslySetInnerHTML という、その名の通り「危険な」穴が、現代のWebアプリケーションにおいて脆弱性の温床となり得るのかを語ろう。
1. 自動エスケープの舞台裏:なぜReactは安全なのか
Reactのレンダリングプロセスは、単なる文字列の連結ではない。JSXはコンパイル時に React.createElement に変換される。このとき、DOM構築の前段階でエスケープが行われる。
具体的には、Reactは内部的に文字列をDOMノードの textContent プロパティを通じてセットする。これは、ブラウザのパーサーに対して「これはHTMLとして解釈してはいけない、単なるテキストノードである」と明示的に指示することと同義だ。つまり、通信プロトコル層でHTMLタグが送られてこようが、ブラウザのレンダリングエンジンはそれを実行コンテキストに組み込むことはない。
これが「自動エスケープ」の正体だ。しかし、この仕組みは「HTMLを動的に構築して表示したい」というCMSやブログ、生成AIのチャットUIといった要件の前に脆くも崩れ去る。そこで登場するのが dangerouslySetInnerHTML だ。
2. dangerouslySetInnerHTMLという「パンドラの箱」
dangerouslySetInnerHTML は、Reactが意図的に用意したバックドアである。これを使用すると、Reactの仮想DOMによるチェックをバイパスし、生のHTML文字列を直接ブラウザの innerHTML に流し込む。
// 危険な実装例
// ユーザーが入力したMarkdownをHTMLに変換して表示するようなケース
function SafeComponent({ rawHtml }) {
// 注意: sanitizerを通していない文字列を渡すのは自殺行為だ
return
}
この実装がなぜ危険か。それは、innerHTML に渡される文字列の中に潜む javascript: プロトコルや、イベントハンドラ属性(onerror や onload)を、ブラウザがそのまま実行してしまうからだ。
3. 深層防御:サニタイズの限界とガードレイルの設計
もし、どうしてもHTMLをレンダリングする必要があるなら、生の入力をそのまま渡すことは絶対に許されない。ここで必要となるのは、OWASPが推奨するようなDOMベースのサニタイザー(DOMPurifyなど)を通すことだが、私はさらに一歩踏み込んだ「アーキテクチャ上の防衛」を提案したい。
推奨される防御ロジック
単に DOMPurify を使うだけでなく、CSP(Content Security Policy)と組み合わせることで、万が一の漏れを物理的に防ぐ必要がある。
import DOMPurify from 'dompurify';
function SanitizedComponent({ rawHtml }) {
// DOMPurifyはホワイトリスト方式で不要なタグや属性を徹底的に除去する
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], // 必要最小限のタグのみ許可
ALLOWED_ATTR: [] // 属性(onclick等)は一切許可しない
});
return
;}
CSPによる多層防御
たとえサニタイザーをすり抜けるような未知のゼロデイ脆弱性がHTML内に存在したとしても、HTTPヘッダーで強固なCSPを敷いていれば被害を最小化できる。
CSPの例: インラインスクリプトを一切禁止する
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
4. 生成AI時代の新たな脅威:プロンプトインジェクションとの交差点
最後に、現在我々が直面している最も厄介なリスクについて触れておこう。それは、生成AIをバックエンドに持つシステムにおいて、AIが生成した「推論結果(HTML)」を dangerouslySetInnerHTML に流し込むケースだ。
AIが毒入りのHTMLを生成するように操作される「プロンプトインジェクション」が発生した場合、それは単なる入力検証のミスではなく、アプリケーションのロジックそのものが汚染されることを意味する。AIの出力は「信頼できるデータ」ではない。AIが生成した文字列こそ、最も厳格にサニタイズし、サンドボックス環境で評価すべき対象なのだ。
結論:プロの矜持として
dangerouslySetInnerHTML を使うことは、爆弾の導火線に火をつけて手元に置くようなものだ。どうしても必要なら、以下の3点を徹底せよ。
1. サニタイズは出力の直前に行うこと(二重処理を恐れるな)。
2. サニタイザーのホワイトリストは、極限まで狭く定義すること(利便性を捨てよ)。
3. CSPによる「実行のガードレイル」を必ず導入すること(ブラウザ側の防衛を信じろ)。
セキュリティは、魔法のようなツールで解決できるものではない。泥臭い入力の追跡と、設計思想の細部へのこだわりが、強固なインフラを形作るのだ。君たちのコードが、真に堅牢であることを期待している。
コメント