2026年9月14日

2026年9月14日

WordPressでSameSite=StrictとCSP nonceを組み合わせる多層防御の設定方法

SameSite=Strict と CSP nonce は守る相手が違うため、組み合わせると効果が出ます。SameSite は「他サイトから送られてくるリクエスト(CSRF)」に対して Cookie を渡さない仕組みで、CSP nonce は「自サイトのページに紛れ込んだスクリプト(XSS)」を実行させない仕組みです。この記事では、WordPress の管理用 Cookie にだけ SameSite=Strict を付ける方法と、フロントエンドに nonce 付きの CSP を段階的に導入する方法を、つまずきやすい点と確認方法を含めて解説します。

3つの防御がそれぞれ何を止めるか

まず役割を整理します。ここが曖昧なままだと「SameSite を入れたから WordPress の nonce チェックは要らない」といった誤った判断につながります。

  • SameSite 属性(ブラウザ側の CSRF 対策):別サイトを起点にしたリクエストに Cookie を付けないようブラウザに指示します。Strict はリンクをクリックした遷移でも送らず、Lax はトップレベルの GET 遷移では送ります。
  • WordPress の nonce と権限チェック(サーバー側の CSRF 対策):wp_verify_nonce() や check_admin_referer() と current_user_can() で、正規の画面から本人が操作したかをサーバーが確かめます。古いブラウザや設定に左右されないため、SameSite を入れても省略しません。
  • CSP nonce(XSS の被害を抑える対策):レスポンスごとに予測できない値を発行し、その値を持つ <script> だけを実行させます。

名前が似ていますが、WordPress の nonce(wp_create_nonce() が作るフォーム用のトークン)と CSP の nonce(スクリプトに付ける使い捨ての値)は別物です。

組み合わせる理由は、それぞれの弱点を補い合うからです。XSS が成立すると、攻撃スクリプトは自サイトのページ内で動くので「同じサイトからのリクエスト」として Cookie が送られ、SameSite は役に立ちません。その XSS を止めるのが CSP です。逆に CSP は他サイトからのフォーム送信を止められないので、そちらは SameSite とサーバー側のチェックが受け持ちます。

WordPressのCookieとSameSiteの現状

WordPress のログイン Cookie は主に3種類です(名前の末尾にはサイトごとのハッシュが付きます)。

  • wordpress_sec_…:HTTPS 環境の認証 Cookie。パスは /wp-admin とプラグインのディレクトリに限定され、管理画面の操作に使われます。
  • wordpress_logged_in_…:ログイン状態を示す Cookie。サイト全体のパスで送られ、管理バーの表示や REST API の認証に使われます。
  • wp-settings-…:管理画面の表示設定などを保存する Cookie。PHP だけでなく管理画面の JavaScript からも書き込まれるため、後述の PHP 側の処理で付けた属性は、JavaScript が書き直した時点で失われることがあります。

WordPress コアはログイン用の Cookie に HttpOnly(HTTPS なら Secure も)を付けますが、SameSite 属性は明示していません(2026年10月現在)。属性がない Cookie の扱いはブラウザによって異なり、Chromium 系は Lax として扱いますが、他のブラウザが同じとは限りません。そこで、明示的に付けるのがこの記事の方針です。

なぜ管理用Cookieだけを「Strict」にするのか

すべての Cookie を Strict にすると、検索結果や SNS からサイトに来たログイン中のユーザーが、最初の1ページだけ「ログアウトしたように見える」状態になります。管理バーが消える程度ならまだしも、会員サイトでは混乱のもとです。

一方、wordpress_sec_… は管理画面専用なので、Strict にしても影響を受けるのは「外部から管理画面へのリンクを開いたとき」だけです。具体的には、メールやチャットに貼られた wp-admin/post.php?post=123&action=edit のようなリンクを開くと、最初のリクエストに認証 Cookie が付かず、ログイン画面に転送されて再ログインを求められます。管理者にとっては手間ですが、「外部ページを踏んだだけで管理画面の操作が実行される」タイプの攻撃に対する防御が厚くなります。

まとめると、wordpress_sec_… は Strict、wordpress_logged_in_… と wp-settings-… は Lax、という分け方が実用上のバランスです。外部の決済サービスや SSO から POST で戻ってくる処理がある場合は、Lax でも Cookie が送られないことがあります。外部連携の挙動はサービスごとに違うため、該当する場合は各サービスの開発者向けドキュメントで Cookie の要件を確認し、ステージングでその流れを一通り試してから本番に入れてください。

mu-pluginでWordPressのCookieにSameSiteを付ける

コアには SameSite を指定するフィルターがないため、PHP の header_register_callback() を使い、ヘッダーを送る直前に Set-Cookie を書き換えます。wp_set_auth_cookie() はプラガブル関数なので上書きもできますが、コアの更新に追随できなくなるため勧めません。

作業前にファイルのバックアップを取り、ステージングで試してから本番に入れます。wp-content/mu-plugins/samesite-cookies.php として保存します。

<?php
/**
 * Plugin Name: SameSite for WordPress cookies
 * Description: wordpress_sec_ に Strict、その他の WordPress Cookie に Lax を付与する。
 */

if ( PHP_SAPI === 'cli' ) {
	return;
}

header_register_callback( function () {
	$cookies = array();
	foreach ( headers_list() as $header ) {
		if ( 0 === stripos( $header, 'Set-Cookie:' ) ) {
			$cookies[] = trim( substr( $header, strlen( 'Set-Cookie:' ) ) );
		}
	}
	if ( empty( $cookies ) ) {
		return;
	}

	header_remove( 'Set-Cookie' );

	foreach ( $cookies as $cookie ) {
		$name = trim( (string) strtok( $cookie, '=' ) );

		if ( false === stripos( $cookie, 'samesite=' ) ) {
			if ( 0 === strpos( $name, 'wordpress_sec_' ) ) {
				$cookie .= '; SameSite=Strict';
			} elseif ( 0 === strpos( $name, 'wordpress_' ) || 0 === strpos( $name, 'wp-settings' ) ) {
				$cookie .= '; SameSite=Lax';
			}
		}

		header( 'Set-Cookie: ' . $cookie, false );
	}
} );

この方法の注意点は2つです。header_register_callback() は1つしか登録できず、後から登録されたものが優先されます。ほかのプラグインが同じ関数を使っていると、どちらかが効かなくなります。また、すでに SameSite を指定している Cookie(プラグインが SameSite=None を付けたものなど)には手を加えないようにしてあります。

確認は、ブラウザの開発者ツールで行います。いったんログアウトしてからログインし直し、Application(Chrome)やストレージ(Firefox)のパネルで Cookie の一覧を開いて、wordpress_sec_… の SameSite 列が Strict、wordpress_logged_in_… が Lax になっていれば成功です。属性はログイン時に発行された Cookie に付くので、既存のセッションには反映されません。

万一ログインできなくなった場合は、FTP や SSH でこの mu-plugin ファイルを削除(または名前を変更)すれば元に戻ります。

CSP nonceをフロントエンドに導入する

WordPress 5.7 で wp_get_script_tag() や wp_get_inline_script_tag() などのスクリプトタグ出力関数と、属性を追加できる wp_script_attributes・wp_inline_script_attributes フィルターが入りました。WordPress 6.4 では、wp_enqueue_script() や wp_add_inline_script() で登録したスクリプトもこれらの関数経由で出力されるようになったため、フィルターで nonce を一括で付けられます。

nonce はレスポンスごとに1つ生成し、ヘッダーと全スクリプトで同じ値を使います。send_headers アクションはテンプレート出力より先に動くので、静的変数に保持して使い回します。

<?php
/**
 * Plugin Name: CSP nonce (front-end)
 */

function mysite_csp_nonce() {
	static $nonce = null;
	if ( null === $nonce ) {
		$nonce = base64_encode( random_bytes( 16 ) );
	}
	return $nonce;
}

add_action( 'send_headers', function () {
	if ( is_admin() ) {
		return;
	}
	$nonce  = mysite_csp_nonce();
	$policy = implode( '; ', array(
		"script-src 'nonce-{$nonce}' 'strict-dynamic' https: 'unsafe-inline'",
		"object-src 'none'",
		"base-uri 'self'",
	) );
	// まずは報告のみ。問題がなくなってから Content-Security-Policy に切り替える
	header( 'Content-Security-Policy-Report-Only: ' . $policy );
} );

add_filter( 'wp_script_attributes', function ( $attributes ) {
	$attributes['nonce'] = mysite_csp_nonce();
	return $attributes;
} );

add_filter( 'wp_inline_script_attributes', function ( $attributes, $data ) {
	$attributes['nonce'] = mysite_csp_nonce();
	return $attributes;
}, 10, 2 );

ポリシーの中身について補足します。'strict-dynamic' は「nonce 付きのスクリプトが読み込んだスクリプトも信頼する」指定で、広告タグやアクセス解析のように別のスクリプトを動的に読み込む仕組みと相性がよくなります。これを理解するブラウザでは、https: と 'unsafe-inline' は無視されます。この2つは CSP Level 3 に対応していない古いブラウザ向けの後方互換用で、新しいブラウザでの安全性は下がりません。object-src 'none' と base-uri 'self' は、nonce を回避する古典的な手口を塞ぐために一緒に入れておく項目です。

管理画面は対象から外しています。ブロックエディターやプラグインの設定画面はインラインスクリプトやインラインのイベントハンドラーに依存している部分があり、厳しい CSP を当てると壊れやすいためです。管理画面の保護は、前半の SameSite とサーバー側のチェックが担います。

段階的に有効化する手順

いきなり Content-Security-Policy で強制すると、どこかのスクリプトが止まってサイトの機能が壊れることがあります。次の順に進めます。

  1. 上のコードのまま Report-Only で有効にし、主要なページ(トップ、記事、固定ページ、フォーム、検索、カートや会員ページなど)を開いて、開発者ツールのコンソールに出る CSP 違反の報告を確認する。
  2. 違反の原因を直す。多いのは、テーマのテンプレートに直接書かれた <script>、プラグインが echo で出しているスクリプト、onclick のようなインラインのイベントハンドラーです。テーマ側は wp_enqueue_script() と wp_add_inline_script() に書き換えます(子テーマで対応します)。プラグイン側は作者に報告するか、代替のプラグインを検討します。
  3. 違反が出なくなったら、ヘッダー名を Content-Security-Policy に変えて強制モードにする。数日はエラーの問い合わせやフォーム送信の件数に変化がないか見守る。

ここでやってはいけないのが、出力バッファで HTML 中のすべての <script> タグに機械的に nonce を付ける方法です。こうすると、攻撃者が注入したスクリプトにまで nonce が付き、CSP の意味がなくなります。

報告をサーバーで集めたい場合は report-uri や report-to ディレクティブで送信先を指定できます。送られてくる報告には閲覧者の URL などが含まれるので、外部の収集サービスを使う場合はプライバシーポリシーでの扱いも確認してください。

ページキャッシュとの相性に注意する

nonce 方式で最も見落とされやすいのがページキャッシュです。キャッシュプラグインや CDN が HTML を保存すると、その中の nonce が全員に同じ値で配られます。値が固定されると予測できない値という前提が崩れ、さらにヘッダー側の nonce と HTML 側の nonce がずれてスクリプトが全部止まることもあります。

対処は次のいずれかです。

  • nonce を使うページをページキャッシュの対象から外す(ログイン中ユーザーやフォームのあるページに限定する、など)。
  • キャッシュを配信する側(nginx や CDN のエッジ処理)で、リクエストごとに nonce を生成して HTML とヘッダーの両方に差し込む。構成が複雑になるため、使っている CDN のドキュメントで対応方法を確認してから進めます。
  • 変化しないインラインスクリプトだけなら、nonce ではなくハッシュ('sha256-…')で許可する方法もあります。

キャッシュを有効にしたサイトでは、同じページを2回読み込み、HTML の nonce 値が毎回変わっているかを確認してください。

まとめ

  • SameSite は他サイトからのリクエスト、CSP nonce は自サイトに紛れ込んだスクリプトを止める。役割が違うので組み合わせ、WordPress の nonce と権限チェックも併用する。
  • wordpress_sec_… は Strict、wordpress_logged_in_… などは Lax にすると、使い勝手を大きく損なわずに管理画面の防御を厚くできる。
  • コアに SameSite のフィルターはないため、mu-plugin で header_register_callback() を使って付与する。戻すときはファイルを削除する。
  • CSP nonce は wp_script_attributes と wp_inline_script_attributes で付け、Report-Only から始めて違反を潰してから強制する。
  • ページキャッシュがあると nonce が固定化するため、キャッシュの対象外にするか、配信側で毎回生成する。

お気軽にご相談ください

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