【実務・中級編】Reactにおける自動エスケープの仕組みとdangerouslySetInnerHTMLの危険性 – アプリケーションセキュリティ & 安全な開発防御ガイド

Reactの「自動エスケープ」を過信してはいけない:dangerouslySetInnerHTMLが引き起こす悪夢と防衛術

やあ、現場の最前線でコードと向き合うエンジニア諸君。

「Reactを使っているからXSS(クロスサイトスクリプティング)は自動的に防げる」――そう安心しきっていないか?
確かに、ReactはデフォルトでJSX内の変数を文字列として処理し、HTMLエンティティに変換してくれる。だが、それはあくまで「Reactの流儀」に従っている場合に限る。

今日は、開発者が利便性のために使う、しかし最も脆弱性を生み出しやすい禁断のAPI、dangerouslySetInnerHTMLの「本当の怖さ」と、それをどう封じ込めるかという実務の話をしよう。

—

1. なぜReactは安全なのか?そして、なぜ壊されるのか

Reactが標準で安全なのは、{name} のように変数を埋め込む際、Reactが内部的に document.createTextNode を呼び出しているからだ。タグやスクリプトは単なる「文字列」として描画されるため、実行されることはない。

しかし、CMSから取得したHTMLコンテンツを表示したい、リッチテキストエディタの出力結果を出したい、といった場面で、開発者はしばしば dangerouslySetInnerHTML というプロパティに手を出す。

このプロパティは、Reactの守護壁を物理的に破壊する。「私はHTMLを直接DOMに流し込むから、セキュリティなんて知ったことか」とReactに命令しているのと同じだ。この瞬間、Reactの自動エスケープ機能は完全に無力化される。

—

2. 実践:攻撃者はこうしてシステムを乗っ取る(PoC)

もし、ユーザーが投稿したコメントをサニタイズせずに dangerouslySetInnerHTML で表示していたらどうなるか。攻撃者は以下のようなペイロードを仕込む。

このコードがレンダリングされた瞬間、 タグの読み込みエラーが発生し、onerror イベントが発火する。結果として、管理者のセッションCookieが攻撃者のサーバーに送信されることになる。これがXSSの基本であり、多くのプロダクトがこれで沈んできた。

—

3. 防御の鉄則:DOMPurifyによる武装

dangerouslySetInnerHTML を使うなら、「汚れたHTMLを無害化する」というプロセスを自前で実装するしかない。車輪の再発明は避け、業界標準である dompurify を使え。これは議論の余地がないルールだ。

安全な実装サンプル(React)

まず、ライブラリをインストールする。
npm install dompurify
npm install @types/dompurify --save-dev

そして、以下のように実装する。

import React from ‘react’;
import DOMPurify from ‘dompurify’;

const SafeHTMLRenderer = ({ rawHtml }) => {
// DOMPurify.sanitize で悪意のあるスクリプトを除去する
// ALLOWED_TAGS や ALLOWED_ATTR で必要最小限の許可に絞るのがコツだ
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [‘b’, ‘i’, ‘em’, ‘strong’, ‘p’],
ALLOWED_ATTR: [] // 属性を空にすればonerror等のイベントも排除できる
});

return (


);
};

ポイント: 許可するタグは最小限にすること。「何でも許可する」設定でサニタイズした気になっているエンジニアが多いが、それは「鍵をかけていない金庫」と同じだ。

—

4. 追撃の一手:CSP(Content Security Policy)による多層防御

アプリケーション側の対策だけでは足りない。万が一、脆弱性が残っていた場合の「最後の防衛線」がCSPだ。Nginxやサーバー側で以下のHTTPヘッダーを付与しておけ。

Nginxの設定例:

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

これにより、仮に攻撃者が注入した

securityintronationalをフォローする

コメント

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