1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CSPで既存サイトを壊さず段階導入する手順(Report-Only→enforce・nginx/Apache対応)

1
Last updated at Posted at 2026-06-23

CSP(Content-Security-Policy)はXSSやデータ窃取を大幅に緩和できる強力なヘッダですが、いきなり有効化すると正規のJavaScriptやCSS、外部フォント、解析タグまでブロックしてサイトが壊れます。これが「CSPは難しい」と言われる最大の理由です。

この記事は、既存の本番サイトを止めずにCSPを導入したいWeb担当者・サイト運営者・制作会社向けに、Content-Security-Policy-Report-Onlyで安全に観測してから段階的にenforceへ切り替える手順を、nginx / Apache / WordPress のコピペ設定つきでまとめたものです。新規構築ではなく「もう動いているサイトに後から入れる」ケースを想定しています。

なぜ「いきなりenforce」は失敗するのか

CSPには2つのヘッダがあります。

ヘッダ 挙動
Content-Security-Policy ポリシー違反のリソースを実際にブロックする(enforce)
Content-Security-Policy-Report-Only ブロックせず、違反だけをレポートとして送る(観測専用)

本番サイトには、自分で書いたJSのほかに、Google Analytics・広告タグ・チャットウィジェット・埋め込み動画・CDN配信のCSS/フォントなど、把握しきれていない外部リソースがたくさん読み込まれています。いきなりenforceで配信すると、これらが一斉に止まってレイアウト崩壊や機能停止を起こします。

そこで 「まずReport-Onlyで何が読み込まれているかを洗い出す → ポリシーを育てる → enforceに昇格する」 という段階導入が定石になります。全体の流れは次の4ステップです。

Step 1  Report-Only でゆるいポリシーを配信し、違反レポートを集める(1〜2週間)
Step 2  集まったレポートを読み、正規リソースを許可リストに足してポリシーを固める
Step 3  インライン script/style を nonce 方式へ移し、'unsafe-inline' を外す
Step 4  Report-Only → Content-Security-Policy(enforce)へ切り替える

Step 1: Report-Only で観測を始める

最初はほぼ何も制限しないゆるいポリシーをReport-Onlyで配信し、レポート送信先(エンドポイント)だけ設定します。違反レポートの送信先は、新しい report-to と従来の report-uri両方を指定するのが安全です。report-to はChrome/Edgeで使われ、report-uri はそれ以外(2026年時点でもFirefoxはCSPのreport-to未対応)へのフォールバックになります。

nginx の場合

Reporting-Endpoints ヘッダでエンドポイント名を定義し、CSPから参照します。

# server {} ブロック内
# 違反レポートの送信先を定義(report-to / report-uri 兼用)
add_header Reporting-Endpoints 'csp-endpoint="https://example.com/csp-report"' always;

# まずは default-src だけゆるく許可し、違反だけ観測する Report-Only
add_header Content-Security-Policy-Report-Only "default-src 'self' https: 'unsafe-inline' 'unsafe-eval'; report-uri https://example.com/csp-report; report-to csp-endpoint" always;

always を付けないとエラーページ(4xx/5xx)でヘッダが付かないため、必ず付けます。

Apache の場合

# .htaccess または <VirtualHost> 内(要 mod_headers)
Header always set Reporting-Endpoints "csp-endpoint=\"https://example.com/csp-report\""
Header always set Content-Security-Policy-Report-Only "default-src 'self' https: 'unsafe-inline' 'unsafe-eval'; report-uri https://example.com/csp-report; report-to csp-endpoint"

WordPress の場合

サーバ設定を触れない共有ホスティングなどでは、functions.php(子テーマ)からヘッダを送れます。

// 子テーマの functions.php
add_action('send_headers', function () {
    if (is_admin()) {
        return; // 管理画面は除外(エディタ等が壊れやすいため)
    }
    header("Reporting-Endpoints: csp-endpoint=\"https://example.com/csp-report\"");
    header("Content-Security-Policy-Report-Only: default-src 'self' https: 'unsafe-inline' 'unsafe-eval'; report-uri https://example.com/csp-report; report-to csp-endpoint");
});

レポートの受け口は、自前で用意するなら「POSTされたJSONをログに追記するだけ」の小さなエンドポイントで十分です。運用に乗せるなら専用のレポート収集サービスを使うと、違反の集計・グルーピングが楽になります。

Step 2: 違反レポートを読み、ポリシーを育てる

ブラウザは違反のたびに、エンドポイントへJSONをPOSTします(report-to形式の例)。

{
  "type": "csp-violation",
  "body": {
    "documentURL": "https://example.com/contact/",
    "effectiveDirective": "script-src",
    "blockedURL": "https://www.googletagmanager.com/gtag/js",
    "disposition": "report",
    "originalPolicy": "default-src 'self' https: ..."
  }
}

読むべきは主に次の3つです。

  • effectiveDirective:どのディレクティブで引っかかったか(script-src / style-src / img-src / connect-src など)
  • blockedURL:ブロック対象になったリソースのURL(=許可するか判断する対象)
  • disposition:report ならReport-Only(観測中)、enforce なら実ブロック

レポートに出てきた正規のドメインだけを、対応するディレクティブの許可リストへ足していきます。たとえば上の例なら、解析タグを使い続けるために script-srchttps://www.googletagmanager.com を追加します。この作業を1〜2週間繰り返すと、サイトが実際に使っているリソースの全体像が見えてきます。

default-src 'self';
script-src  'self' https://www.googletagmanager.com https://www.google-analytics.com;
style-src   'self' https://fonts.googleapis.com;
font-src    'self' https://fonts.gstatic.com;
img-src     'self' data: https://www.google-analytics.com;
connect-src 'self' https://www.google-analytics.com;

ポイントは、default-src'self' に絞り、必要なものだけを各 *-src で個別に開けること。最初のゆるい https:'unsafe-eval' は、観測が終わったら必ず外していきます。

Step 3: インライン script/style を nonce 方式へ移す

ここが段階導入で一番手間のかかる山場です。<script>...</script> のようにHTMLに直接書かれたJSstyle="..."インラインスタイルは、'unsafe-inline' を許可しない限りブロックされます。しかし 'unsafe-inline' を付けたままだとXSS対策としてのCSPがほぼ無意味になります。

安全側の解決策は nonce(ノンス:リクエストごとに変わる使い捨ての乱数) です。リクエストのたびに乱数を生成し、ヘッダと <script> の両方に同じ値を付けることで、「サーバが意図して出力したスクリプトだけ」を実行許可します。

<?php
// リクエストごとに 16 バイトの乱数を base64 で生成
$nonce = base64_encode(random_bytes(16));

// CSP ヘッダ側:'unsafe-inline' の代わりに nonce を許可
header("Content-Security-Policy: "
    . "default-src 'self'; "
    . "script-src 'self' 'nonce-{$nonce}'; "
    . "style-src 'self' 'nonce-{$nonce}'");
?>
<!-- HTML 側:同じ nonce を付けたインライン script だけが実行される -->
<script nonce="<?php echo htmlspecialchars($nonce, ENT_QUOTES); ?>">
  console.log('このスクリプトは nonce 一致で許可される');
</script>

nonceは毎リクエストで必ず作り直すのが鉄則です(固定値や使い回しは、推測されると意味がなくなります)。外部の固定ファイルに対しては、ファイル内容のハッシュを許可する 'sha256-...' 方式も使えます。どうしてもインラインを書き換えられない古いコードが残る場合は、そのページだけ段階導入を遅らせ、先に書き換え可能なページからenforceしていきます。

Step 4: Report-Only から enforce へ切り替える

レポートに新しい違反がほぼ出なくなり、'unsafe-inline' / 'unsafe-eval' / https: といったゆるい指定を外しきれたら、いよいよ昇格です。やることはヘッダ名を変えるだけです。

# Before(観測):ブロックしない
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'nonce-...'; ..." always;

# After(本番):同じポリシーを実ブロックで適用。report-to は残して監視を継続
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-...'; report-to csp-endpoint" always;

切り替え後も report-to / report-uri は残し、enforce後の違反も監視し続けるのが安全です。新しいタグの追加やサイト改修で、後からブロックが発生することはよくあります。心配な場合は、まず一部のページや一部のディレクティブ(例:img-src だけ先にenforce)から始めて、影響範囲を区切るとリスクを下げられます。

設定後の確認コマンド

ヘッダが意図どおり返っているかは curl -I で確認できます。

# レスポンスヘッダだけ取得して CSP 行を確認
curl -sI https://example.com/ | grep -i -E 'content-security-policy|reporting-endpoints'

ブラウザのDevTools(Console / Network タブ)でも、ブロックされたリソースと違反したディレクティブが赤字で表示されるので、report段階・enforce段階どちらでも目視確認に使えます。

よくある詰まりどころ

  • 管理画面・エディタが壊れる:WordPress管理画面やCMSのエディタはインラインJSを多用するため、is_admin() 等で管理画面を除外してから本番側だけ締めます。
  • add_header が効かない(nginx):location ブロック内で別の add_header を書くと、親の add_headerすべて打ち消される仕様です。CSPを書く階層に他のヘッダもまとめて書きます。
  • CDN/プラグインが勝手にヘッダを付ける:二重に Content-Security-Policy が付くと厳しい方が適用されます。Step 1 の curl -I で重複していないか必ず確認します。

まとめ

  • CSPはReport-Onlyで観測 → ポリシーを育てる → enforceへ昇格の順で、本番を壊さず導入する。
  • report-toreport-uri両方指定し、Reporting-Endpoints でエンドポイントを定義する。
  • インラインJS/CSSは 'unsafe-inline' を避け、毎リクエスト生成のnonceで許可する。
  • enforce後も監視を継続し、心配ならディレクトリやディレクティブ単位で段階的に締める。

関連記事

本記事のCSP設定やセキュリティヘッダが正しく入っているかは、Webサイトを9つの守りでまるごと守る「サイトドック」の月次の定期健診でまとめて確認できます → https://sitedock.jp

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?