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-src へ https://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に直接書かれたJSや style="..." のインラインスタイルは、'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-toとreport-uriを両方指定し、Reporting-Endpointsでエンドポイントを定義する。 - インラインJS/CSSは
'unsafe-inline'を避け、毎リクエスト生成のnonceで許可する。 - enforce後も監視を継続し、心配ならディレクトリやディレクティブ単位で段階的に締める。
関連記事
- Webサイトに最低限入れるべきHTTPセキュリティヘッダ7種と設定例
- WordPressセキュリティ 最低限やることチェックリスト10
- Cookieのセキュリティ属性 SameSite / Secure / HttpOnly の正しい付け方
本記事のCSP設定やセキュリティヘッダが正しく入っているかは、Webサイトを9つの守りでまるごと守る「サイトドック」の月次の定期健診でまとめて確認できます → https://sitedock.jp