2026年7月1日
2026年7月1日
WordPressにContent Security Policy(CSP)を設定する方法|Report-Onlyから始めてnonceで強化する
WordPressにContent Security Policy(CSP)を導入するときは、いきなり遮断するのではなく、Content-Security-Policy-Report-Only で違反を観察し、許可リストを整えてから本適用に切り替えるのが安全な進め方です。さらにXSS対策として実効性を持たせるには、'unsafe-inline' に頼らずnonceでスクリプトを許可する形を目指します。この記事では、WordPress特有のつまずき(インラインスクリプト、管理画面、ページキャッシュ)を踏まえて、段階的に設定する手順と確認方法を解説します。
CSPで何が防げて、WordPressでは何が難しいのか
CSPは、ページが読み込んでよいスクリプト・スタイル・画像・接続先などをHTTPヘッダーで宣言し、それ以外をブラウザに拒否させるしくみです。たとえばコメント欄やプラグインの脆弱性を突かれて <script> タグが埋め込まれても、そのスクリプトが許可されていなければ実行されません。
ただしWordPressでは、次の理由で厳しいポリシーをそのまま適用しにくい事情があります。
- インラインスクリプトが多い:
wp_localize_script()などで出力される設定値や、プラグインが直接書き出す<script>があります。 - インラインスタイルが多い: ブロックのグローバルスタイルや装飾は
<style>やstyle属性としてHTMLに入ります。 - 外部サービス: アクセス解析、YouTube埋め込み、Webフォントなど、許可すべきドメインがサイトごとに違います。
- 管理画面: ブロックエディターにフロントと同じポリシーを適用すると、編集画面が動かなくなることがあります。
このため、「まず観察する」「管理画面は対象外にする」「スタイルは緩め、スクリプトを重点的に締める」という方針で進めます。
主なディレクティブ
よく使うディレクティブは次のとおりです。
default-src: 個別の指定がない種類のリソースに適用される既定値script-src: JavaScriptの読み込み元。XSS対策の中心style-src: CSSの読み込み元img-src/font-src/media-src: 画像・フォント・音声動画の読み込み元connect-src:fetch()やXHR、WebSocketの接続先。アクセス解析の送信先もここframe-src: ページ内に埋め込むiframeの読み込み元frame-ancestors: 自サイトを他サイトのiframeに埋め込ませるかどうか(クリックジャッキング対策)object-src: Flashなどのプラグイン要素。'none'にするのが一般的base-uri/form-action:<base>タグの指定先、フォームの送信先
ステップ1: Report-Onlyで現状を観察する
最初は Content-Security-Policy-Report-Only ヘッダーを使います。このヘッダーは違反を報告するだけで、何もブロックしません。サイトの見た目や動作を壊さずに、どのリソースがポリシーに引っかかるかを洗い出せます。
Nginxでフロントにだけ付ける例です。Googleアナリティクス(タグマネージャー経由)とGoogle Fonts、YouTube埋め込みを使うサイトを想定しています。設定ファイルは変更前に複製し、sudo nginx -t で構文を確認してから再読み込みしてください。
# server ブロック内(1行で記述)
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: https:; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://www.googletagmanager.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;
server ブロックに書くと /wp-admin/ にも同じヘッダーが付きますが、Report-Onlyの段階では動作に影響しません。本適用の段階では管理画面を外す必要があるため、後述のPHPで出力する方法に切り替えるか、フロント用の location にだけ書きます。なお location 内に add_header を書くと、server ブロック側の add_header はその location で送られなくなる点にも注意してください。
Apacheなら .htaccess やバーチャルホストの設定に次のように書きます(mod_headers が必要です)。
<IfModule mod_headers.c>
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
</IfModule>
ヘッダーを付けたら、開発者ツールのコンソールを開いて主要なページを巡回します。違反があると、どのディレクティブのどのURLが原因かが警告として表示されます。記事ページ、お問い合わせフォーム、検索結果、WooCommerceならカートと決済画面など、使われている機能を一通り開いてください。違反を収集し続けたい場合は report-uri(または report-to)で報告の送信先を指定できますが、受け取る仕組みを用意する必要があります。
ステップ2: 許可リストを整えて本適用する
観察で見つかった正当な読み込み元をポリシーに追加し、警告が出なくなったらヘッダー名を Content-Security-Policy に変えて本適用します。いきなり全体に適用せず、アクセスの少ない時間帯に切り替え、問い合わせフォームの送信や埋め込み動画の再生など、実際の操作まで確認してください。
ここで正直に押さえておきたいのは、script-src に 'unsafe-inline' を含めたポリシーは、XSS対策としての効果が限られるという点です。攻撃者が埋め込んだインラインスクリプトも実行できてしまうため、守れるのは「許可していない外部ドメインからのスクリプト読み込み」が中心になります。それでも外部ドメインの制限や object-src 'none'、frame-ancestors には意味がありますが、本来の効果を得るには次のnonce方式に進みます。
ステップ3: nonceでインラインスクリプトを許可する
nonceは、リクエストごとに生成する推測できない値です。ヘッダーの script-src に 'nonce-ランダム値' を書き、正規の <script> タグにだけ同じ nonce 属性を付けると、その値を持たないスクリプトは実行されません。
WordPress 5.7で追加された wp_script_attributes と wp_inline_script_attributes フィルターを使うと、WordPressのスクリプト出力関数を通る <script> タグに属性を追加できます。比較的新しいバージョンでは、wp_enqueue_script() で登録したスクリプトやインラインスクリプトもこれらの関数を通って出力されます(2026年10月現在)。
<?php
// リクエストごとに1つのnonceを生成して使い回す
function wpdr_csp_nonce() {
static $nonce = null;
if ( null === $nonce ) {
$nonce = base64_encode( random_bytes( 16 ) );
}
return $nonce;
}
function wpdr_add_nonce_attr( $attributes ) {
$attributes['nonce'] = wpdr_csp_nonce();
return $attributes;
}
add_filter( 'wp_script_attributes', 'wpdr_add_nonce_attr' );
add_filter( 'wp_inline_script_attributes', 'wpdr_add_nonce_attr' );
// フロントのページにだけヘッダーを送る(send_headers は管理画面やwp-login.phpでは実行されない)
add_action( 'send_headers', function () {
$nonce = wpdr_csp_nonce();
$policy = "script-src 'nonce-{$nonce}' 'strict-dynamic' 'unsafe-inline' https:; "
. "object-src 'none'; base-uri 'self'; frame-ancestors 'self'";
header( 'Content-Security-Policy-Report-Only: ' . $policy );
} );
このポリシーの 'unsafe-inline' と https: は、nonceに対応していない古いブラウザ向けの予備です。nonceや 'strict-dynamic' を理解するブラウザでは、これらは無視されます。'strict-dynamic' は、nonceで許可されたスクリプトが動的に読み込む別のスクリプトも許可する指定で、タグマネージャーのように後から他のスクリプトを読み込むものと相性がよい書き方です。
ここでも最初はReport-Onlyで動かし、コンソールに出る違反を確認します。違反として残りやすいのは次のものです。
- **テーマやプラグインが
echoで直接書き出す<script>**: WordPressの関数を通らないためnonceが付きません。wp_enqueue_script()やwp_print_inline_script_tag()を使う形に修正するか、作者に対応を依頼します。 - **
onclickなどのインラインイベント属性**: nonceでは許可できないため、JavaScript側でイベントを登録する形に書き換えます。 - タグマネージャーのカスタムHTMLタグ: タグマネージャーの設定側でnonceを引き継ぐ対応が必要です。
ページキャッシュとnonceの相性
nonce方式で最も注意が必要なのがページキャッシュです。nonceはリクエストごとに変わる前提ですが、ページキャッシュはHTMLを使い回すため、キャッシュされたHTMLの nonce 属性と、そのとき送られるヘッダーのnonceが一致しなくなります。本適用するとスクリプトがすべて止まり、サイトが動かなくなります。
対処としては、キャッシュプラグインやCDN側でHTMLとヘッダーを一緒にキャッシュする(同じnonceが使い回されるため、毎回変わる値ほどの強さはなくなります)、エッジでnonceを差し替える仕組みを使う、といった方法がありますが、構成によって難易度が大きく変わります。ページキャッシュを使っているサイトでは、まず許可リスト方式で外部ドメインを絞り、nonce方式は検証環境で十分に試してから判断するのが現実的です。
設定を確認する方法
curl -I https://example.com/を実行し、意図したヘッダーが1つだけ返っているかを確認します。サーバー設定とプラグインの両方でCSPを出していると、ヘッダーが2つになり、両方の条件を満たすものしか許可されなくなります。- 開発者ツールのコンソールで違反の警告が出ていないかを、主要なページで確認します。
- GoogleのCSP Evaluatorなどのツールにポリシーを貼り付けると、
'unsafe-inline'の影響などの弱点を指摘してくれます。
<meta http-equiv="Content-Security-Policy"> でもCSPは指定できますが、metaタグではReport-Onlyや frame-ancestors が使えません。WordPressではHTTPヘッダーで出力する方法を基本にしてください。
まとめ
- CSPは
Content-Security-Policy-Report-Onlyで観察してから本適用に切り替える - WordPressはインラインスクリプト・スタイルが多く、管理画面はポリシーの対象から外すのが無難
'unsafe-inline'を含む許可リスト方式は外部ドメインの制限が中心で、XSS対策としては限定的wp_script_attributes/wp_inline_script_attributesフィルターでnonceを付け、'strict-dynamic'と組み合わせると実効性が上がる- nonce方式はページキャッシュと衝突するため、構成を確かめてから導入する
curl -Iでヘッダーが重複していないか、コンソールで違反が残っていないかを確認する