ブログ一覧に戻る
Reactアプリをサブディレクトリに配置したらCSPで地獄を見た話
2026/2/3
2026/2/23
25
Web開発Tips

Reactアプリをサブディレクトリに配置したらCSPで地獄を見た話

シェア

既存サイトにReactアプリを統合しようとして、Content Security Policy(CSP)の競合問題にハマった経験を共有します。同じ悩みを持つ方の参考になれば幸いです。

やりたかったこと

既存のNext.jsサイト akashi-n.com に、別途開発したReactアプリ(MoneyFlip)を /moneyflip/ というサブディレクトリで公開したいと考えました。

example.com/           ← Next.jsメインサイト
example.com/moneyflip/ ← Reactアプリ(静的HTML)

構成としては以下のようなイメージです。

┌─────────────────────────────────────────┐
│         Netlify(ホスティング)          │
├─────────────────────────────────────────┤
│  /                                      │
│  ├── Next.jsアプリ(メインサイト)       │
│  │   └── CSP: 親サイト用の設定          │
│  │                                      │
│  └── /moneyflip/                        │
│      └── Reactアプリ(静的HTML)         │
│          └── CSP: アプリ固有の設定が必要 │
└─────────────────────────────────────────┘

単体開発時のCSP設定

MoneyFlipは為替変換アプリで、外部APIや国旗画像などのリソースを使用しています。単体開発時は index.html<meta> タグでCSPを設定していました。

<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://pagead2.googlesyndication.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https://flagcdn.com https://www.google-analytics.com;
  connect-src 'self' https://open.er-api.com https://www.google-analytics.com;
">

このアプリで必要だった外部リソース

ディレクティブ

リソース

用途

script-src

https://pagead2.googlesyndication.com

Google AdSense

script-src

https://www.googletagmanager.com

Google Analytics

script-src

'unsafe-inline'

GA/AdSense用(理想はnonceだが...)

img-src

https://flagcdn.com

国旗画像の取得

img-src

https://www.google-analytics.com

GAのビーコン

connect-src

https://open.er-api.com

為替レートAPI

connect-src

https://analytics.google.com

GA

font-src

https://fonts.googleapis.com

Google Fonts(MUI)

font-src

https://fonts.gstatic.com

Google Fonts

単体では問題なく動作していました。

親サイト統合で発生した問題

いざ親サイトに統合してデプロイすると、コンソールが真っ赤になりました。

Refused to load the script 'https://pagead2.googlesyndication.com/...' 
because it violates the following Content Security Policy directive: 
"script-src 'self' 'unsafe-inline' https://www.googletagmanager.com"
Refused to connect to 'https://open.er-api.com/v6/latest/USD' 
because it violates the following Content Security Policy directive: 
"connect-src 'self' https://api.microcms.io..."

原因:metaタグとHTTPヘッダーの両方が適用される

調査の結果、問題の原因がわかりました。

  1. 親サイト(Next.js)netlify.toml でサーバーレベルのCSPヘッダーを設定
  2. 子アプリ(React)index.html のmetaタグでCSPを設定
  3. 両方のポリシーが適用される

よくある誤解

「HTTPヘッダーがmetaタグより優先される」と思いがちですが、実際は両方のポリシーが適用されます。

つまり、両方で許可されているリソースしか読み込めません。片方だけで許可しても、もう片方で許可されていなければブロックされます。

親サイト(HTTPヘッダー): script-src 'self' https://www.googletagmanager.com
子アプリ(metaタグ):     script-src 'self' https://pagead2.googlesyndication.com

→ 実際に許可されるのは 'self' のみ(両方で許可されているもの)
→ googletagmanager.com も pagead2.googlesyndication.com もブロックされる

親サイトのCSPに https://flagcdn.comhttps://open.er-api.com が含まれておらず、子アプリのCSPにも親サイトが必要とするリソースが含まれていないため、お互いの機能がブロックされていたのです。

解決策:CSPを一箇所で管理する

CSPは一箇所で管理するのがベストプラクティスです。サブディレクトリにアプリを配置する場合は、子アプリのmetaタグを削除し、親サイトのサーバー設定でパス別にCSPを定義しましょう。

Netlifyの場合(netlify.toml)

# より具体的なパスを先に定義(重要!)
[[headers]]
  for = "/moneyflip"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com https://pagead2.googlesyndication.com https://*.google.com https://*.googleapis.com https://*.gstatic.com https://*.doubleclick.net https://*.adtrafficquality.google; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://flagcdn.com https://www.google-analytics.com https://*.google.com https://*.googleapis.com https://*.gstatic.com https://*.doubleclick.net https://*.adtrafficquality.google; connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://open.er-api.com https://*.google.com https://*.googleapis.com https://*.gstatic.com https://*.doubleclick.net https://*.adtrafficquality.google; frame-src 'self' https://*.doubleclick.net https://*.adtrafficquality.google https://*.google.com; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests"

[[headers]]
  for = "/moneyflip/*"
  [headers.values]
    Content-Security-Policy = "..."  # 同じ内容

# メインサイト用(最後に定義)
[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "..."  # メインサイト用の設定

ポイント: Netlifyでは上から順にマッチングされるため、具体的なパス(/moneyflip)を先に、汎用的なパス(/*)を後に定義します。

Nginxの場合

server {
    # メインサイト用
    location / {
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com ...";
    }

    # MoneyFlip用
    location /moneyflip {
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com https://www.googletagmanager.com ...";
    }
}

Apache(.htaccess)の場合

# メインサイト用
<IfModule mod_headers.c>
    Header set Content-Security-Policy "default-src 'self'; ..."
</IfModule>

# MoneyFlip用
<Directory "/var/www/html/moneyflip">
    <IfModule mod_headers.c>
        Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://pagead2.googlesyndication.com ..."
    </IfModule>
</Directory>

まとめ・チェックリスト

サブディレクトリにReactアプリを統合する際のチェックリストです。

統合前の確認

  • [ ] 子アプリの index.html からCSP metaタグを削除したか
  • [ ] 子アプリが必要とする外部リソースをすべてリストアップしたか
  • [ ] サーバー設定でパス別CSPを定義したか
  • [ ] パスの優先順位は正しいか(具体的なパスを先に)

外部サービス別の追加設定

Google Analytics(GA4)を使う場合

  • script-src: https://www.googletagmanager.com, https://www.google-analytics.com
  • connect-src: https://www.google-analytics.com, https://analytics.google.com
  • img-src: https://www.google-analytics.com

Google AdSenseを使う場合

  • script-src: https://pagead2.googlesyndication.com, https://*.doubleclick.net, https://*.adtrafficquality.google
  • img-src: https://*.doubleclick.net, https://*.adtrafficquality.google
  • connect-src: https://*.doubleclick.net, https://*.adtrafficquality.google
  • frame-src: https://*.doubleclick.net, https://*.adtrafficquality.google, https://*.google.com

Google Fontsを使う場合

  • style-src: https://fonts.googleapis.com
  • font-src: https://fonts.gstatic.com

おわりに

CSPの問題は、エラーメッセージを見れば原因は明確なのですが、「なぜ自分の設定が効かないのか」を理解するまでに時間がかかりました。

ポイントは以下の3つです。

  1. 両方のポリシーが適用される:HTTPヘッダーとmetaタグは競合ではなく、両方の AND(交差)が取られる
  2. 結果的に厳しくなる:両方で許可されているリソースしか読み込めない
  3. 一箇所で管理する:子アプリのmetaタグは削除し、サーバー設定でパス別にCSPを定義

同じ問題でハマっている方の参考になれば幸いです。


実際にこの方法で作ったのが MoneyFlip です。Claude Code で開発した通貨換算アプリで、PWA対応でホーム画面に追加して使えます。

カテゴリ:Web開発Tipsタグ:ReactNetlifyNext.js

この記事が役に立ったらフォローしてください