Reactアプリ セキュリティ対策の基本|今日から使えるチェックリスト

「Reactでアプリを作っているが、セキュリティ対策は何から手をつければいいのか分からない」——海外の開発者コミュニティ(Reddit r/reactjs)でも同様の疑問が繰り返し議論されており、「React特有のチェックリストがあるのか、それとも一般的なWebセキュリティの原則をそのまま適用すればいいのか」という問いに対して、現場のエンジニアたちから数多くの実践的な回答が寄せられていました。その議論を読み解くと、意見はバラバラなようでいて、実は一貫した共通認識に収れんしていきます。それは「フロントエンドだけでは絶対にアプリを守り切れない」という前提と、「その上でReact特有の落とし穴が確かに存在する」という2点です。

本記事では、この2つの軸を土台に、Reactアプリのセキュリティ対策の基本を、仕組みの理解からリスクの整理、具体的な実装手順、コード例、チェックリストまで一気通貫で解説します。初めてReactでプロダクトを本番運用する方や、チームのセキュリティ方針を整理したい方に向けた内容です。

なぜ「Reactだから安全」ではないのか——JSXの自動エスケープが守ってくれる範囲

Reactには、JSXの中に変数を埋め込むと自動的にHTMLエスケープしてくれるという優れた特性があります。{"{userInput}"}のように出力するだけで、<script>タグのような文字列はそのままテキストとして表示され、DOMに実行可能なコードとして注入されることはありません。これは、Reactが「デフォルトでXSS(クロスサイトスクリプティング)に強い」と言われる理由です。

しかし、この安全性には明確な境界線があります。境界線の外側に出た瞬間、Reactの自動防御は一切効かなくなります。具体的には次の3つが代表的な「境界線の外」です。

  • dangerouslySetInnerHTMLを使って生のHTML文字列をDOMに挿入する場合
  • ユーザーが入力した値を、そのままコンポーネントのpropsとして渡し、そのprops経由で間接的にdangerouslySetInnerHTMLが呼ばれてしまう場合
  • ユーザーが入力した値を、そのまま<a>タグのhref属性に渡す場合(javascript:スキームによるコード実行)

つまり「Reactを使っていれば自動的に安全」なのではなく、「Reactのデフォルトの挙動から逸脱する箇所だけをピンポイントで守れば、大部分のXSSは防げる」というのが実態に近い理解です。まずこの仕組みを正確に押さえることが、React特有のセキュリティ対策の出発点になります。

Reactアプリで実際に起こりやすいリスクと課題

1. dangerouslySetInnerHTMLの誤用によるXSS

WYSIWYGエディタやリッチテキスト表示、CMSから取得したHTMLの描画などでdangerouslySetInnerHTMLを使うケースは珍しくありません。しかし、サニタイズ(無害化)せずにユーザー由来・外部由来のHTMLをそのまま流し込むと、悪意のあるスクリプトがそのまま実行されてしまいます。

2. クライアント側の出し分けだけで「認可」を実装してしまう

「管理者ロールのユーザーにだけ管理画面のリンクを表示する」といった実装を、フロントエンドの条件分岐だけで完結させてしまうケースです。ブラウザの開発者ツールやAPIへの直接リクエストは誰でも実行できるため、UI側で隠しているだけの機能は「隠れているだけで、実際には誰でも呼び出せる」状態になります。UIの出し分けはあくまでUXの向上であり、セキュリティ対策そのものではありません。

3. CORS設定を「アクセス制御」だと誤解している

CORS(Cross-Origin Resource Sharing)は、ブラウザが悪意あるサイトから正規ユーザーのクッキー等を使って裏側でAPIを叩く、CSRF的な挙動をブロックするための仕組みです。しかし、CORSの判定に使われるOriginヘッダーは、ブラウザ以外のクライアント(curlやサーバー間通信など)からは簡単に偽装できます。「特定オリジンからのリクエストしか許可していないから安全」という思い込みは危険で、CORS設定はAPI自体の認証・認可の代替にはなりません。

4. トークンやシークレットの不適切な保存・埋め込み

APIキーやシークレットをフロントエンドのコードに直書きすると、ビルド後のJavaScriptバンドルにそのまま残り、ブラウザの開発者ツールから誰でも閲覧できてしまいます。また、認証トークンをlocalStorageに保存する実装は手軽な反面、XSSが一箇所でも成立するとトークンごと盗まれるリスクを抱えます。

5. 依存パッケージの放置によるサプライチェーンリスク

npmエコシステムに依存するReactアプリは、自分たちが書いたコード以外に大量のサードパーティコードを実行しています。脆弱性が公表されたパッケージを更新せずに使い続けることは、自社のコードをどれだけ堅牢にしても意味を失わせるリスクです。

6. 通信の平文化・セキュリティヘッダーの未設定

HTTPS化されていない通信、Content Security Policy(CSP)やその他セキュリティヘッダーの未設定も、React自体とは直接関係ないものの、Reactアプリ全体の安全性を左右する重要な要素です。

具体的な対策手順

手順1:フロントエンドを信頼しないという原則を徹底する

すべての対策の土台になる考え方です。ブラウザで動くコードはユーザーの手元で自由に書き換え・観察・実行できるという前提に立ちましょう。バリデーション、認可判定、金額計算などの「守るべきロジック」は、必ずサーバー側でも独立して再検証してください。フロントエンドでの入力チェックはあくまでUX向上のためのものであり、セキュリティの最終防衛線はサーバー側に置きます。

手順2:dangerouslySetInnerHTMLは使わない。使う場合は必ずサニタイズする

可能な限りdangerouslySetInnerHTML自体を避けるのが最善です。どうしても必要な場合(WYSIWYGエディタのプレビュー表示など)は、DOMPurifyのような実績のあるサニタイズライブラリを使い、フロントエンドとバックエンドの両方でサニタイズを行う「二重の防御」を意識してください。

import DOMPurify from 'dompurify';

function ArticleBody({ rawHtml }) {
  const cleanHtml = DOMPurify.sanitize(rawHtml, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href'],
  });

  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}

手順3:ユーザー入力をpropsやhrefに直接流し込まない

ユーザーが自由入力できる文字列を、そのままリンク先や他コンポーネントのpropsに渡す設計は避けましょう。リンクのhrefは、許可するスキーム(https:mailto:など)をあらかじめ定義したallowlist方式で検証し、javascript:のような危険なスキームを排除します。

const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:'];

function isSafeUrl(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    return ALLOWED_PROTOCOLS.includes(parsed.protocol);
  } catch {
    return false;
  }
}

function SafeLink({ href, children }) {
  if (!isSafeUrl(href)) {
    return <span>{children}</span>;
  }
  return <a href={href} rel="noopener noreferrer">{children}</a>;
}

手順4:認可はUIの出し分けとAPI側の両方で行う

管理者向けメニューやボタンをUI上で非表示にするのは構いませんが、対応するAPIエンドポイント側でも必ずロールやパーミッションを検証してください。「フロントエンドで隠しているから大丈夫」という発想は捨て、「フロントエンドの表示はUX、実際の防御はAPI側」という役割分担を徹底します。

手順5:認証情報の保存方法を見直す

可能であれば、認証トークンはJavaScriptからアクセスできないHTTP OnlyかつSecure属性付きのCookieに保持し、CSRF対策(SameSite属性の設定やCSRFトークンの発行)と組み合わせるのが望ましい構成です。やむを得ずlocalStorageやメモリ上で管理する場合は、XSS対策(手順2・3)をより一層厳格に実施する必要があります。パスワードなどの機密情報をクライアント側に保存しないことも基本です。

手順6:CORSを正しく理解し、認可の代替にしない

CORSはあくまで「ブラウザ経由の意図しないクロスオリジンリクエストからユーザーを守る仕組み」と理解し、許可オリジンを絞り込む設定は行いつつも、APIそのものには別途、トークン検証などの認証・認可を必ず実装してください。

手順7:通信経路とヘッダーを固める

全ページ・全APIをHTTPS化し、混在コンテンツ(HTTP資源の読み込み)を排除します。加えて、Content-Security-Policy、X-Content-Type-Options、Strict-Transport-Security、Referrer-Policyなどのセキュリティヘッダーをサーバー側で設定し、万一XSSが発生した場合の被害範囲を最小化します。

手順8:依存パッケージの脆弱性を継続的に監視する

npm auditやDependabotのような仕組みを導入し、脆弱性が報告されたパッケージのアップデートを定期的に行うフローをチームの運用に組み込みます。放置期間が長くなるほど攻撃の対象になりやすくなるため、検知から対応までのリードタイムを短くすることが重要です。

手順9:シークレットをビルド成果物に含めない

APIキーやシークレットは環境変数として扱い、フロントエンドのビルドに埋め込む値は「ブラウザから見られても問題ないもの」に限定します。本当に秘匿すべき値はサーバー側(BFFやバックエンドAPI)でのみ保持し、フロントエンドからは直接参照させない設計にします。

すぐに使えるセキュリティチェックリスト

  • 入力値の検証・認可判定をフロントエンドだけで完結させず、サーバー側でも必ず再検証している
  • dangerouslySetInnerHTMLを使っていない、または使う場合はDOMPurify等でサニタイズしている
  • ユーザー入力を直接hrefやpropsに渡さず、allowlistで検証している
  • 管理者向け機能などの認可判定をAPI側でも実装している(UIの出し分けだけに頼っていない)
  • 認証トークンの保存方法(HTTP Only Cookie等)とCSRF対策を見直している
  • CORSを認可の代替として扱っていない
  • 全通信がHTTPS化され、CSP等のセキュリティヘッダーを設定している
  • 依存パッケージの脆弱性を定期的にチェック・更新している
  • APIキーやシークレットをフロントエンドのビルド成果物に含めていない
  • 難読化(minify/terserなど)は補助的な手段であり、根本対策ではないと認識している

まとめ

Reactアプリのセキュリティ対策は、特別な魔法のようなReact専用テクニックを覚えることではありません。むしろ本質は「フロントエンドは信頼できない実行環境である」という原則を土台に、Reactが自動で守ってくれる範囲(JSXのエスケープ)と、開発者が意識的に守る必要がある範囲(dangerouslySetInnerHTML、href、props経由の注入、クライアント側だけの認可判定など)を正確に切り分けることにあります。

そのうえで、認証・認可・通信の暗号化・依存パッケージ管理といった一般的なWebアプリケーションセキュリティの原則をReactの実装に落とし込んでいけば、過度に難しく考えなくても着実にリスクを下げることができます。今回のチェックリストを、開発フローやコードレビューの基準に取り入れてみてください。

コメント

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