2026年8月13日

2026年8月13日

WordPressのNonceでセキュリティを強化する方法(フォーム・Ajax・REST APIの実装例)

WordPressのNonceは、フォーム送信やAjaxリクエストが「本人が自分のサイトの画面から意図して送ったものか」を確かめるための使い捨て風のトークンで、主にCSRF(クロスサイトリクエストフォージェリ)対策に使います。ただし、Nonceだけでは安全になりません。権限チェック・入力のサニタイズ・出力のエスケープと組み合わせて初めて効果を発揮します。この記事では、Nonceの仕組みと限界を整理したうえで、管理画面フォーム、Ajax、REST APIそれぞれの正しい実装例を紹介します。

Nonceの仕組みと「できないこと」

WordPressのNonceは wp_create_nonce( 'アクション名' ) で生成され、アクション名・ログイン中のユーザーID・ログインセッション・時間の区切りをもとに計算されます。受け取った側は wp_verify_nonce() で同じ計算をして一致するかを調べます。攻撃者は被害者のセッションに結び付いた値を知り得ないため、別サイトから勝手に送信させるCSRFを防げます。

名前に反して、WordPressのNonceは「1回しか使えない値」ではありません。既定では約24時間の有効期間内なら何度でも通ります(nonce_life フィルターで変更できます)。wp_verify_nonce() は、生成から前半12時間なら 1、後半なら 2、無効なら false を返します。

Nonceが防がないものも理解しておく必要があります。

  • 権限の確認にはならない:Nonceは「誰が」ではなく「意図した操作か」を確かめるものです。操作してよいユーザーかどうかは current_user_can() で別に確認します。
  • ログアウト状態では効果が限られる:未ログインユーザーは全員ユーザーIDが0として扱われるため、同じNonceを共有します。公開フォームのスパム対策には、レート制限やCAPTCHA系の仕組みなどを別に用意します。
  • XSSには無力:サイト内でスクリプトを実行されると、Nonceも読み取られます。出力のエスケープが前提です。

実装の基本ルール

Nonceを使う処理は、次の順番で書くと抜け漏れを防げます。

  1. 送信元(フォームやJavaScript)でNonceを付ける
  2. 受信側で Nonceを検証 する
  3. 権限を確認 する(current_user_can())
  4. 入力値を wp_unslash() してから サニタイズ する
  5. 処理を実行し、画面に出す値は エスケープ する

アクション名は myprefix_delete_item_123 のように、操作内容や対象IDを含めて具体的にすると、別の操作のNonceを使い回されにくくなります。コードは独自プラグインか mu-plugin に置き、ステージング環境で動作を確かめてから本番に反映してください。

管理画面のフォームで使う

管理画面の独自フォームから設定を保存する例です。送信先には admin-post.php を使い、admin_post_{action} フックで受け取ります。

// フォームの出力(管理メニューのページ内で呼ぶ)
function myprefix_render_form() {
	$value = get_option( 'myprefix_message', '' );
	?>
	<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
		<input type="hidden" name="action" value="myprefix_save_message">
		<?php wp_nonce_field( 'myprefix_save_message', 'myprefix_nonce' ); ?>
		<input type="text" name="myprefix_message" value="<?php echo esc_attr( $value ); ?>">
		<?php submit_button( '保存' ); ?>
	</form>
	<?php
}

// 送信の受け取り
add_action( 'admin_post_myprefix_save_message', 'myprefix_handle_save_message' );
function myprefix_handle_save_message() {
	check_admin_referer( 'myprefix_save_message', 'myprefix_nonce' );

	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( esc_html__( 'この操作を行う権限がありません。', 'myprefix' ), 403 );
	}

	$message = isset( $_POST['myprefix_message'] )
		? sanitize_text_field( wp_unslash( $_POST['myprefix_message'] ) )
		: '';

	update_option( 'myprefix_message', $message, false );

	wp_safe_redirect( add_query_arg( 'updated', '1', wp_get_referer() ?: admin_url() ) );
	exit;
}

wp_nonce_field() は隠しフィールドを出力し、check_admin_referer() は検証に失敗すると処理を止めてエラー画面を表示します。リダイレクト後に exit を書き忘れると後続の処理が動いてしまうため注意してください。

削除リンクのようにURLで操作する場合は、wp_nonce_url( $url, 'myprefix_delete_item_' . $item_id ) でクエリにNonceを付け、受け取り側で同じアクション名を指定して検証します。データを変更する操作は、可能であればGETではなくPOSTで送るほうが安全です。

Ajax(admin-ajax.php)で使う

JavaScriptからAjaxで送る場合は、Nonceをスクリプトに渡しておき、リクエストに含めます。

add_action( 'admin_enqueue_scripts', 'myprefix_enqueue_admin_js' );
function myprefix_enqueue_admin_js() {
	wp_enqueue_script( 'myprefix-admin', plugins_url( 'admin.js', __FILE__ ), array(), '1.0.0', true );
	wp_add_inline_script(
		'myprefix-admin',
		'const myprefixAjax = ' . wp_json_encode( array(
			'url'   => admin_url( 'admin-ajax.php' ),
			'nonce' => wp_create_nonce( 'myprefix_toggle' ),
		) ) . ';',
		'before'
	);
}

add_action( 'wp_ajax_myprefix_toggle', 'myprefix_ajax_toggle' );
function myprefix_ajax_toggle() {
	check_ajax_referer( 'myprefix_toggle', 'nonce' );

	if ( ! current_user_can( 'edit_posts' ) ) {
		wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
	}

	$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
	if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
		wp_send_json_error( array( 'message' => 'invalid post' ), 400 );
	}

	// ここで処理を行う
	wp_send_json_success( array( 'post_id' => $post_id ) );
}
const body = new URLSearchParams( {
	action: 'myprefix_toggle',
	nonce: myprefixAjax.nonce,
	post_id: '123',
} );
fetch( myprefixAjax.url, { method: 'POST', credentials: 'same-origin', body } )
	.then( ( res ) => res.json() )
	.then( ( data ) => console.log( data ) );

check_ajax_referer() の第2引数は、Nonceを入れたリクエストパラメータ名です。未ログインユーザー向けの wp_ajax_nopriv_ フックを追加する場合は、前述のとおりNonceの効果が限られるため、入力の検証やレート制限をより厳しくしてください。

REST APIで使う

ログイン中のユーザーがブラウザから REST API を呼ぶ場合、WordPressはCookie認証に加えて wp_rest アクションのNonceを要求します。Nonceがないリクエストは未ログインとして扱われます。

fetch( '/wp-json/myprefix/v1/items', {
	method: 'POST',
	headers: {
		'Content-Type': 'application/json',
		'X-WP-Nonce': myprefixRest.nonce, // PHP 側で wp_create_nonce( 'wp_rest' ) を渡しておく
	},
	body: JSON.stringify( { title: 'sample' } ),
} );

独自エンドポイントでは、register_rest_route() の permission_callback で current_user_can() を確認してください。Nonceは REST API 側が検証するため、エンドポイント内で改めて検証する必要は通常ありません。ブロックエディター用のスクリプトで @wordpress/api-fetch を使う場合は、Nonceの付与が自動で行われます。

「リンクの有効期限が切れました」と出るときの確認

Nonceの検証に失敗すると、管理画面では「リンクの有効期限が切れました」などのメッセージが表示されます。表記はバージョンによって異なる場合があります(2026年10月現在)。主な原因は次のとおりです。

  • ページキャッシュに古いNonceが残っている:ログインユーザーや会員ページがキャッシュされていないか、キャッシュプラグインの除外設定を確認します。
  • 画面を長時間開いたままにしていた:有効期間を過ぎたNonceは無効です。再読み込みして再送信します。
  • アクション名やパラメータ名の不一致:生成側と検証側の文字列を見比べます。
  • ログインし直した:セッションが変わると以前のNonceは通りません。

まとめ

  • NonceはCSRF対策のためのトークンで、約24時間は繰り返し使えます。
  • Nonceは権限の確認ではありません。current_user_can() と組み合わせます。
  • 処理は「Nonce検証→権限確認→サニタイズ→処理→エスケープ」の順で書きます。
  • フォームは wp_nonce_field() と check_admin_referer()、Ajaxは check_ajax_referer()、REST APIは X-WP-Nonce ヘッダーと permission_callback を使います。
  • 未ログインユーザー向けの処理では、Nonce以外の防御を別に用意します。

お気軽にご相談ください

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