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を使う処理は、次の順番で書くと抜け漏れを防げます。
- 送信元(フォームやJavaScript)でNonceを付ける
- 受信側で Nonceを検証 する
- 権限を確認 する(
current_user_can()) - 入力値を
wp_unslash()してから サニタイズ する - 処理を実行し、画面に出す値は エスケープ する
アクション名は 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以外の防御を別に用意します。