2026年5月20日

2026年5月20日

WordPressのContent Security Policy(CSP)設定方法|Report-Onlyからnonceまで

Content Security Policy(CSP)は、ページがどこからスクリプト・スタイル・画像などを読み込んでよいかをブラウザーに伝えるHTTPヘッダーです。正しく設定すると、XSS(クロスサイトスクリプティング)で埋め込まれたスクリプトが実行されにくくなります。一方で、設定を誤るとサイトの機能が止まります。この記事では、WordPressで次の順に進める方法を説明します。

  1. Content-Security-Policy-Report-Only で、何が違反になるかを把握する
  2. 違反レポートを受け取る
  3. 許可リスト型のポリシーを適用する
  4. 必要に応じて、より厳しいnonce型のポリシーに移る

コードは2026年10月時点のWordPress 6系を前提にしています。

2つの方式の違い

方式 書き方の例 長所 短所
許可リスト型 script-src 'self' 'unsafe-inline' https://www.googletagmanager.com 既存のテーマやプラグインが壊れにくい 'unsafe-inline' を含むと、埋め込まれたインラインスクリプトも実行されるため、XSS対策としての効果は限られる
nonce型 script-src 'nonce-ランダムな値' 'strict-dynamic' 正しいnonceが付いたスクリプトだけが実行され、XSS対策の効果が大きい nonceを付けられないインラインスクリプトやイベント属性が動かなくなる。ページキャッシュとの相性が悪い

まず許可リスト型で始め、違反がなくなってから、必要ならnonce型を検討するのが現実的です。

手順1:Report-Onlyで違反を把握する

Content-Security-Policy-Report-Only ヘッダーは、ポリシーに違反するリソースをブロックせず、違反の報告だけをします。サイトの動作を変えずに、どのリソースが引っかかるかを確認できます。

テーマの functions.php、またはテーマを変えても残るよう wp-content/mu-plugins/ に置いたPHPファイルに書きます。

<?php
// CSP を Report-Only で送る(ブロックはせず、違反を報告するだけ)
add_action( 'send_headers', function () {
	$directives = array(
		"default-src 'self'",
		"script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com",
		"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
		"img-src 'self' data: https:",
		"font-src 'self' data: https://fonts.gstatic.com",
		"connect-src 'self' https://www.google-analytics.com",
		"frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com",
		"object-src 'none'",
		"base-uri 'self'",
		"form-action 'self'",
		"frame-ancestors 'self'",
		'report-uri ' . esc_url_raw( rest_url( 'csp/v1/report' ) ),
	);
	header( 'Content-Security-Policy-Report-Only: ' . implode( '; ', $directives ) );
} );

外部ドメインは例です。実際に使っているサービスに合わせて増減してください。report-uri には、次の手順で作る受け取り口のURLを rest_url() で指定しています。パーマリンク設定が「基本」でも正しいURLになります。

違反は、ブラウザーの開発者ツールの「コンソール」にも表示されます。トップページ、投稿、固定ページ、問い合わせフォーム、埋め込み動画のあるページなど、主要なページを一通り開いて確認してください。

主なディレクティブ

ディレクティブ 制御するもの
default-src 個別に指定していない種類のリソースの既定値
script-src JavaScript
style-src CSS
img-src 画像。data: はインライン画像、https: はHTTPSのすべての画像を許可
font-src Webフォント
connect-src fetch() やAjaxの接続先
frame-src iframe で埋め込む先
object-src <object>・<embed>。'none' で禁止
base-uri <base> タグで指定できるURL
form-action フォームの送信先
frame-ancestors 自分のページを iframe に埋め込めるサイト(クリックジャッキング対策)

違反レポートに eval が出る場合は、テーマやプラグインが eval() などを使っています。そのスクリプトが必要なら script-src に 'unsafe-eval' を足しますが、安全性は下がります。

手順2:違反レポートの受け取り口を作る

ブラウザーは、違反が起きると report-uri に指定したURLへJSON(Content-Typeは application/csp-report)をPOSTします。WordPressのREST APIで受け取り、PHPのエラーログに書き出す例です。

<?php
// CSP 違反レポートを受け取り、エラーログに記録する
add_action( 'rest_api_init', function () {
	register_rest_route(
		'csp/v1',
		'/report',
		array(
			'methods'             => 'POST',
			'permission_callback' => '__return_true', // ブラウザーから認証なしで送られるため
			'callback'            => function ( WP_REST_Request $request ) {
				$body = $request->get_body();
				// 大きすぎる送信は記録しない
				if ( strlen( $body ) > 0 && strlen( $body ) <= 10000 ) {
					$report = json_decode( $body, true );
					if ( is_array( $report ) && isset( $report['csp-report'] ) ) {
						$r = $report['csp-report'];
						error_log(
							sprintf(
								'CSP violation: %s blocked=%s page=%s',
								$r['violated-directive'] ?? '',
								$r['blocked-uri'] ?? '',
								$r['document-uri'] ?? ''
							)
						);
					}
				}
				return new WP_REST_Response( null, 204 );
			},
		)
	);
} );

ログの出力先は、wp-config.php で WP_DEBUG_LOG を有効にしている場合は wp-content/debug.log、そうでなければサーバーのPHPエラーログです。この受け取り口は誰でも送信できるため、アクセスが多いサイトではログが大きくなります。確認が終わったら、ポリシーから report-uri を外すか、この受け取り口を削除してください。

なお、report-uri は新しい仕様では report-to に置き換えが進んでいますが、2026年10月時点でも主要なブラウザーで使えます。

手順3:許可リスト型のポリシーを適用する

レポートを見ながら、必要なドメインを追加し、不要なものを削ります。主要なページで違反が出なくなったら、ヘッダー名を Content-Security-Policy に変えて適用します。

<?php
// 手順1のコードのヘッダー名だけを変える
header( 'Content-Security-Policy: ' . implode( '; ', $directives ) );

適用後も、しばらくは report-uri を残して違反を監視し、管理者がふだん使わない機能(会員登録、決済、検索など)でも動作を確認してください。

手順4:nonce型のポリシーに移る(上級者向け)

nonce型では、リクエストごとにランダムな値(nonce)を作り、CSPヘッダーと、許可する <script> タグの両方に同じ値を付けます。値が一致しないスクリプトは実行されません。

WordPressでは、wp_enqueue_script() で読み込むスクリプトのタグは wp_get_script_tag() で、wp_add_inline_script()・wp_localize_script()・翻訳データなどのインラインスクリプトは wp_get_inline_script_tag() で出力されます。それぞれ wp_script_attributes と wp_inline_script_attributes フィルターで属性を追加できるため、ここにnonceを付けます。script_loader_tag フィルターで文字列を置き換える方法では、wp_localize_script() のデータなど一部のインラインスクリプトにnonceが付かないため、この2つのフィルターを使います。

<?php
// nonce 型の CSP(まず Report-Only で試す)

// リクエストごとに1つの nonce を作る
function my_csp_nonce() {
	static $nonce = null;
	if ( null === $nonce ) {
		$nonce = base64_encode( random_bytes( 16 ) );
	}
	return $nonce;
}

// wp_enqueue_script() で読み込むスクリプトのタグに nonce を付ける
add_filter( 'wp_script_attributes', function ( $attributes ) {
	$attributes['nonce'] = my_csp_nonce();
	return $attributes;
} );

// wp_add_inline_script() などのインラインスクリプトに nonce を付ける
add_filter( 'wp_inline_script_attributes', function ( $attributes ) {
	$attributes['nonce'] = my_csp_nonce();
	return $attributes;
} );

add_action( 'send_headers', function () {
	$directives = array(
		"default-src 'self'",
		"script-src 'nonce-" . my_csp_nonce() . "' 'strict-dynamic'",
		"style-src 'self' 'unsafe-inline'",
		"img-src 'self' data: https:",
		"object-src 'none'",
		"base-uri 'self'",
		'report-uri ' . esc_url_raw( rest_url( 'csp/v1/report' ) ),
	);
	header( 'Content-Security-Policy-Report-Only: ' . implode( '; ', $directives ) );
} );

'strict-dynamic' は、nonceの付いたスクリプトが後から読み込むスクリプトも許可する指定です。Googleタグマネージャーのように、別のスクリプトを読み込むサービスを使う場合に必要になることがあります。'strict-dynamic' に対応したブラウザーでは、script-src に書いたドメインの許可リストは無視されます。

nonce型で動かなくなるもの

nonce型を適用すると、次のスクリプトは実行されません。Report-Onlyの段階でレポートを確認し、一つずつ対処してください。

  • テーマのテンプレートやプラグインが <script> を直接出力しているもの:wp_enqueue_script()・wp_add_inline_script()・wp_print_inline_script_tag() を使う書き方に直す必要があります。header.php に直接貼ったアクセス解析のタグなども該当します
  • onclick="…" などのイベント属性と javascript: で始まるリンク:nonceでは許可できません。addEventListener() を使うスクリプトに書き換えます
  • nonceを付けた後で 'unsafe-inline' を頼りにしているもの:script-src にnonceがあると、ブラウザーは 'unsafe-inline' を無視します。両方を書いても、インラインスクリプトは許可されません

ページキャッシュとの相性

nonceはリクエストごとに変わる必要があります。ページキャッシュのプラグインやCDNがHTMLを保存して返すと、HTMLに埋め込まれたnonceと、ヘッダーのnonceが一致しなくなり、スクリプトが止まります。また、キャッシュによって同じnonceが多くの訪問者に配られると、nonceの意味がなくなります。ページキャッシュを使っているサイトでは、nonce型ではなく許可リスト型を使うか、キャッシュの仕組みがnonceに対応しているかを確認してから導入してください。

手順5:設定を確認する

ヘッダーが送られているかをcurlで確認します。example.com は自分のドメインに置き換えます。

curl -sI https://example.com/ | grep -i "content-security-policy"

ブラウザーの開発者ツールの「ネットワーク」タブでページを選び、レスポンスヘッダーでも確認できます。ポリシーの弱点は、Googleが公開している CSP Evaluator に貼り付けると指摘してくれます。

注意点

  • send_headers はWordPressがフロントのページを表示するときに実行されます。管理画面とログイン画面には、この記事のヘッダーは付きません
  • .htaccess やNginxの設定でもCSPを送っていると、ヘッダーが2つになります。ブラウザーは両方のポリシーを満たすリソースだけを許可するため、意図しないブロックが起きます。設定する場所は1か所にしてください
  • テーマやプラグインを追加・更新したときは、Report-Onlyのレポートやコンソールで違反が増えていないかを確認してください

まとめ

  • 最初は Content-Security-Policy-Report-Only で送り、コンソールと違反レポートで引っかかるリソースを把握する
  • 違反レポートは register_rest_route() で受け取り口を作り、rest_url() で report-uri に指定する
  • 許可リスト型は壊れにくいが、'unsafe-inline' を含む限りXSS対策の効果は限られる
  • nonce型は wp_script_attributes と wp_inline_script_attributes フィルターでnonceを付ける。直接出力された <script>・イベント属性・javascript: リンクは止まり、'unsafe-inline' は無視される
  • ページキャッシュを使うサイトでは、nonce型の導入前にキャッシュとの相性を確認する

関連記事:

お気軽にご相談ください

お見積りへ お問い合わせへ
▲