2026年6月19日

2026年6月19日

WordPressのコンテンツセキュリティポリシー(CSP)をレポートモードで安全にテストする方法

CSP(コンテンツセキュリティポリシー)をいきなり有効にすると、プラグインのスクリプトやアクセス解析タグがブロックされ、サイトの一部が動かなくなることがあります。Content-Security-Policy-Report-Only ヘッダーを使えば、何もブロックせずに「このポリシーなら何が違反になるか」だけを記録できます。この記事では、レポートモードの仕組み、WordPressで違反レポートを受け取る方法、レポートの読み方、本番の強制モードへ切り替える判断までを順に説明します。

レポートモードとは

CSPは、ページが読み込んでよいスクリプト、スタイル、画像、フレームなどの出どころをブラウザに指示する仕組みで、XSS(クロスサイトスクリプティング)の被害を抑えるのに役立ちます。

指示の送り方は2種類あります。

  • Content-Security-Policy: ポリシーに反するリソースを実際にブロックする(強制モード)
  • Content-Security-Policy-Report-Only: ブロックはせず、違反をコンソールに表示し、指定した宛先へレポートを送る(レポートモード)

レポートモードでは訪問者の見た目や動作が変わらないため、本番サイトで実際のアクセスを使って検証できます。また、2つのヘッダーは同時に送れます。今のゆるいポリシーを強制しながら、より厳しい案をレポートモードで試す、という段階的な使い方ができます。

なお、HTMLの <meta http-equiv> で指定するCSPはレポートモードに対応していません。レポートモードはHTTPレスポンスヘッダーで送る必要があります。

最初のポリシー案を作る

最初から完璧なポリシーを書こうとせず、厳しめの案をレポートモードで出して、レポートを見ながら許可先を足していく方が効率的です。出発点の例を示します。

default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
report-uri https://example.com/wp-json/csp/v1/report;
report-to csp-endpoint

実際のヘッダーでは改行せず1行で送ります。report-uri は古い方式ですが対応ブラウザが多く、report-to は新しい Reporting API の方式です。ブラウザによって対応状況が異なるため(2026年10月現在)、両方を書いておくのが一般的です。report-to を使う場合は、宛先の名前を定義する Reporting-Endpoints ヘッダーも一緒に送ります。

WordPressのフロントでは、テーマやプラグインがインラインスクリプトやインラインスタイルを多用しているため、この案ではかなりの数の違反が出るはずです。それで正常です。どこから何が読み込まれているかを洗い出すのが、この段階の目的です。

ヘッダーを送る方法

Webサーバーで送る(おすすめ)

ページキャッシュを使っているサイトでは、キャッシュされたHTMLがPHPを通らずに返されることがあり、PHPで送ったヘッダーが付かない場合があります。そのため、ヘッダーはWebサーバー側で付ける方が、全ページへ漏れなく行き渡ります。作業前に設定ファイルのバックアップを取ってください。

Apache(mod_headers)の例です。

<IfModule mod_headers.c>
    Header always set Reporting-Endpoints "csp-endpoint=\"https://example.com/wp-json/csp/v1/report\""
    Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; report-uri https://example.com/wp-json/csp/v1/report; report-to csp-endpoint"
</IfModule>

Nginx の例です。

add_header Reporting-Endpoints 'csp-endpoint="https://example.com/wp-json/csp/v1/report"' always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; report-uri https://example.com/wp-json/csp/v1/report; report-to csp-endpoint" always;

Nginx の add_header は、子の location で別の add_header を書くと親の設定が引き継がれなくなります。静的ファイル用の location などで独自にヘッダーを付けている場合は、そちらにも書く必要があるか確認してください。

WordPressから送る

サーバー設定を触れない場合は、子テーマの functions.php や mu-plugin から送れます。send_headers はフロントのリクエストで発火するアクションで、管理画面には適用されません。まずはフロントだけで検証するのが無難なので、この範囲で十分です。

<?php
add_action( 'send_headers', function () {
    $endpoint = rest_url( 'csp/v1/report' );
    $policy   = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; "
              . "object-src 'none'; base-uri 'self'; report-uri {$endpoint}; report-to csp-endpoint";

    header( 'Reporting-Endpoints: csp-endpoint="' . esc_url_raw( $endpoint ) . '"' );
    header( 'Content-Security-Policy-Report-Only: ' . $policy );
} );

違反レポートを受け取るエンドポイントを作る

ブラウザは違反を見つけると、指定先へJSONをPOSTします。ここでは REST API にエンドポイントを追加し、要点だけをPHPのエラーログへ書き出します。ブラウザはログイン情報なしで送ってくるため公開エンドポイントになりますが、そのぶん受け取るサイズと件数を制限し、値を無害化してから記録します。

<?php
add_action( 'rest_api_init', function () {
    register_rest_route( 'csp/v1', '/report', array(
        'methods'             => 'POST',
        'callback'            => 'mysite_csp_receive_report',
        'permission_callback' => '__return_true', // ブラウザからの匿名送信を受けるため
    ) );
} );

function mysite_csp_receive_report( WP_REST_Request $request ) {
    // 1分あたりの受付件数に上限を設け、大量送信でログが溢れないようにする
    $count = (int) get_transient( 'mysite_csp_report_count' );
    if ( $count >= 300 ) {
        return new WP_REST_Response( null, 429 );
    }
    set_transient( 'mysite_csp_report_count', $count + 1, MINUTE_IN_SECONDS );

    $body = $request->get_body();
    if ( strlen( $body ) > 20000 ) {
        return new WP_REST_Response( null, 413 );
    }
    $data = json_decode( $body, true );
    if ( ! is_array( $data ) ) {
        return new WP_REST_Response( null, 400 );
    }

    // report-uri 形式は {"csp-report": {...}}、Reporting API 形式は [{"type": ..., "body": {...}}, ...]
    if ( isset( $data['csp-report'] ) && is_array( $data['csp-report'] ) ) {
        $reports = array( $data['csp-report'] );
    } elseif ( wp_is_numeric_array( $data ) ) {
        $reports = array_slice( $data, 0, 20 );
    } else {
        return new WP_REST_Response( null, 400 );
    }

    foreach ( $reports as $report ) {
        if ( ! is_array( $report ) ) {
            continue;
        }
        $b = ( isset( $report['body'] ) && is_array( $report['body'] ) ) ? $report['body'] : $report;
        $pick = function ( array $keys ) use ( $b ) {
            foreach ( $keys as $k ) {
                if ( isset( $b[ $k ] ) && is_scalar( $b[ $k ] ) ) {
                    return sanitize_text_field( (string) $b[ $k ] );
                }
            }
            return '';
        };
        error_log( 'CSP report: ' . wp_json_encode( array(
            'directive' => $pick( array( 'effective-directive', 'effectiveDirective', 'violated-directive' ) ),
            'blocked'   => $pick( array( 'blocked-uri', 'blockedURL' ) ),
            'page'      => $pick( array( 'document-uri', 'documentURL' ) ),
            'source'    => $pick( array( 'source-file', 'sourceFile' ) ),
        ) ) );
    }
    return new WP_REST_Response( null, 204 );
}

report-uri 方式のレポートは application/csp-report という Content-Type で届くため、REST API の JSON 自動解析には頼らず、get_body() で生のデータを読んで json_decode() しています。パーマリンク設定が「基本」の場合、REST API のURLは /?rest_route=/csp/v1/report の形になる点にも注意してください。

アクセスの多いサイトでは、レポートの量がかなり多くなります。自前の収集が負担になる場合は、CSPレポートの収集に対応した外部サービスを使う方法もあります。

レポートを読んで許可先を足す

ログがある程度溜まったら、directive(どのルールに違反したか)と blocked(何がブロック対象になったか)の組み合わせで集計します。読み方の目安は次のとおりです。

  • blocked が外部ドメインのURL: アクセス解析、Webフォント、動画埋め込みなど。必要なものだけ、該当するディレクティブ(script-src、font-src、frame-src など)にドメインを追加する
  • **blocked が inline**: インラインのスクリプトやスタイル。'unsafe-inline' を足すと手軽ですが、XSS対策としての効果が大きく下がる
  • **blocked が eval**: 文字列からコードを実行する処理。使っているライブラリを特定して判断する
  • **chrome-extension などで始まるもの**: 訪問者のブラウザ拡張機能によるもので、サイト側では対処不要なことが多い

インラインスクリプトへの対応として、WordPress 5.7 以降には wp_inline_script_attributes や wp_script_attributes といったフィルターがあり、コアの関数で出力されるスクリプトタグに nonce 属性を付けられます。ただし、すべてのテーマやプラグインがこの仕組みを通してスクリプトを出しているわけではないため、nonce 方式への移行はレポートで残りの違反を確認しながら進めることになります。

管理画面やブロックエディターは、フロントよりも多くのインライン処理を使います。フロントのポリシーを固めてから、管理画面は別のポリシーで検証する方が混乱しません。

強制モードへ切り替える判断

次の条件がそろったら、ヘッダー名を Content-Security-Policy に変えて強制モードへ移ります。

  1. 数日から数週間、通常のアクセスでレポートを集めた
  2. 残っている違反が、拡張機能由来など対処不要なものだけになった
  3. お問い合わせフォーム、決済、ログイン、動画埋め込みなど主要な機能を手動で試した

切り替え後もしばらくは report-uri と report-to を残し、想定外の違反が出ていないか監視を続けます。問題が出たときに備えて、すぐにレポートモードへ戻せるよう、変更前の設定を控えておいてください。

動作確認の方法

ヘッダーが付いているかは、次のコマンドで確認できます。

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

ブラウザの開発者ツールのコンソールでは、レポートモードでの違反が「Report Only」を含むメッセージとして表示されます。ネットワークタブでレポート送信のリクエストが出ているか、エンドポイントが 204 を返しているかも確認してください。Reporting API の送信はまとめて遅れて送られることがあるため、すぐに届かなくても慌てる必要はありません。

まとめ

  • Content-Security-Policy-Report-Only は何もブロックせず違反だけを記録するため、本番でも安全に検証できる
  • レポートモードは meta タグでは使えないため、HTTPヘッダーで送る。ページキャッシュがあるならサーバー側で付けると漏れが少ない
  • 受け口は REST API で作れるが、公開エンドポイントになるのでサイズと件数を制限し、値を無害化して記録する
  • レポートは「どのディレクティブで何が違反したか」で集計し、必要な許可先だけを足す
  • 違反が落ち着き、主要機能を試してから強制モードに切り替え、その後もレポートで監視を続ける

お気軽にご相談ください

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