2026年5月29日
2026年5月29日
WordPressのNonceでセキュリティを強化する方法|フォーム・リンク・Ajax・REST APIでの使い方と限界
WordPressの nonce は、「このリクエストは、本人がこのサイトの画面から意図して送ったものか」を確かめるための、一定時間だけ有効なトークンで、主にクロスサイトリクエストフォージェリ(CSRF)を防ぐために使います。フォームやリンク、Ajax、REST API で値を変更する処理には、nonce の検証と権限チェックをセットで入れるのが基本です。この記事では、nonce の仕組みと有効期間、場面ごとの発行と検証のコード、nonce だけでは防げないこと、キャッシュとの相性や「リンクの有効期限が切れています」と出たときの考え方までを解説します。
nonce が防ぐもの、防がないもの
CSRF は、ログイン中の管理者が攻撃者の用意したページを開いたときに、そのブラウザから自分のサイトへ「設定を変更する」「記事を削除する」といったリクエストを勝手に送らせる攻撃です。ブラウザはログイン中のCookieを自動で付けるため、サーバー側はCookieだけでは本人の操作か区別できません。
nonce は、サイトの画面を表示したときにだけ埋め込まれる値です。攻撃者のページからはこの値を知ることができないので、nonce が正しいリクエストは「自サイトの画面から送られたもの」と判断できます。
一方で、nonce は次のことを保証しません。
- 権限:nonce が正しくても、そのユーザーに操作する権限があるとは限りません。権限の確認は
current_user_can()で別に行います。 - 入力値の安全性:送られた値の検証・サニタイズ、出力時のエスケープは別途必要です。
- ログインしていない利用者の識別:後述のとおり、未ログインの利用者には全員に同じ nonce が発行されます。
つまり nonce は「意図の確認」で、「誰が・何をしてよいか」は権限チェックが担います。両方そろって初めて安全な処理になります。
WordPress の nonce の仕組みと有効期間
WordPress の nonce は、名前のとおりの「一度きりの番号」ではありません。アクション名・ユーザーID・ログインセッションのトークン・時間帯から計算されるハッシュで、有効期間内なら何度でも検証に通ります。
- 既定の有効期間は 24 時間で、12 時間ごとの2つの区切りで管理されます(2026年10月現在)。
nonce_lifeフィルターで変更できます wp_verify_nonce()は、前半の12時間に作られたものなら1、後半なら2、無効ならfalseを返します- ログアウトするとセッションが変わるため、それ以前に発行された nonce は無効になります
アクション名には、'delete_post_' . $post_id のように対象を含めると、別の投稿への操作に流用されるのを防げます。
フォームで使う:wp_nonce_field と check_admin_referer
管理画面に独自の設定フォームを作り、admin-post.php で受け取る例です。
<?php
// フォームの表示側
function myplugin_render_form() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="myplugin_save">
<?php wp_nonce_field( 'myplugin_save_settings', 'myplugin_nonce' ); ?>
<input type="text" name="myplugin_label" value="<?php echo esc_attr( get_option( 'myplugin_label', '' ) ); ?>">
<?php submit_button(); ?>
</form>
<?php
}
// 受け取り側
add_action( 'admin_post_myplugin_save', function () {
check_admin_referer( 'myplugin_save_settings', 'myplugin_nonce' ); // 失敗すると処理を停止
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( '権限がありません。', 'myplugin' ), 403 );
}
$label = isset( $_POST['myplugin_label'] )
? sanitize_text_field( wp_unslash( $_POST['myplugin_label'] ) )
: '';
update_option( 'myplugin_label', $label );
wp_safe_redirect( add_query_arg( 'updated', '1', wp_get_referer() ?: admin_url() ) );
exit;
} );
wp_nonce_field() は hidden の入力欄を出力し、check_admin_referer() はその値を検証して、不正なら処理を止めます。第1引数のアクション名と第2引数の項目名は、発行側と検証側で一致させます。
設定画面を Settings API(register_setting() と settings_fields())で作る場合は、settings_fields() が nonce を自動で出力し、options.php 側で検証されます。自分で nonce を書く必要はありませんが、register_setting() で sanitize_callback を指定して入力値をサニタイズするのは自分の役目です。
投稿の保存時に使う:メタボックス
save_post は自動保存や他のプラグインからの更新でも実行されるため、check_admin_referer() のように処理を止める関数は使わず、wp_verify_nonce() で判定して抜けます。
<?php
add_action( 'save_post', function ( $post_id ) {
if ( ! isset( $_POST['myplugin_meta_nonce'] ) ) {
return;
}
$nonce = sanitize_text_field( wp_unslash( $_POST['myplugin_meta_nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'myplugin_save_meta_' . $post_id ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( isset( $_POST['myplugin_subtitle'] ) ) {
update_post_meta( $post_id, 'myplugin_subtitle', sanitize_text_field( wp_unslash( $_POST['myplugin_subtitle'] ) ) );
}
} );
メタボックスの表示側では wp_nonce_field( 'myplugin_save_meta_' . $post->ID, 'myplugin_meta_nonce' ) を出力しておきます。
リンクで使う:wp_nonce_url
「削除する」「承認する」のように、クリック1つで状態が変わるリンクにも nonce を付けます。
<?php
$url = wp_nonce_url(
admin_url( 'admin-post.php?action=myplugin_delete&item=' . absint( $item_id ) ),
'myplugin_delete_' . absint( $item_id )
);
echo '<a href="' . esc_url( $url ) . '">' . esc_html__( '削除', 'myplugin' ) . '</a>';
受け取り側では check_admin_referer( 'myplugin_delete_' . $item_id ) で検証します。ただし、削除のような重要な操作は、本来 GET ではなく POST で受けるほうが安全です。リンク形式にする場合も、確認画面を挟むと誤操作を減らせます。
Ajax と REST API で使う
admin-ajax.php を使う Ajax では、JS に nonce を渡し、受け取り側で check_ajax_referer() を使います。
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script( 'myplugin-like', plugins_url( 'like.js', __FILE__ ), array(), '1.0.0', array( 'in_footer' => true ) );
wp_add_inline_script( 'myplugin-like', 'const mypluginLike = ' . wp_json_encode( array(
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'myplugin_like' ),
) ) . ';', 'before' );
} );
add_action( 'wp_ajax_myplugin_like', function () {
check_ajax_referer( 'myplugin_like', 'nonce' ); // 失敗すると -1 を返して終了
if ( ! current_user_can( 'read' ) ) {
wp_send_json_error( null, 403 );
}
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
// … いいねを記録する処理 …
wp_send_json_success( array( 'post_id' => $post_id ) );
} );
JS からは fetch() で action=myplugin_like、nonce、post_id を POST します。
REST API をログイン中のユーザーのCookieで呼ぶ場合は、wp_create_nonce( 'wp_rest' ) で作った値を X-WP-Nonce ヘッダーで送ります。この nonce が無いと、WordPressはCookieがあってもリクエストを未ログイン扱いにします。ブロックエディターなど wp-api-fetch を使う画面では、この処理が自動で行われます。独自のエンドポイントでは、nonce とは別に register_rest_route() の permission_callback で権限を確認してください。
つまずきやすい点
ページキャッシュと nonce
nonce を含むページをページキャッシュやCDNが長時間保存すると、有効期間を過ぎた古い nonce が配られ、送信のたびに失敗します。ログイン中のユーザーには通常キャッシュを返さない設定が多いものの、未ログインでも使うフォームでは問題になります。対策としては、そのページをキャッシュの対象から外す、送信直前に Ajax や REST API で nonce を取り直す、キャッシュの保持時間を nonce の有効期間より十分短くする、といった方法があります。
未ログインの利用者では効果が限られる
未ログインの利用者はユーザーIDが 0 として扱われ、同じ時間帯なら全員に同じ nonce が発行されます。お問い合わせフォームのような公開フォームでは、nonce だけに頼らず、ハニーポットや送信回数の制限、スパム対策プラグインを組み合わせます。
「リンクの有効期限が切れています」と表示される
check_admin_referer() が失敗すると、この趣旨のメッセージで処理が止まります(表示される文言はバージョンや言語設定で異なることがあります)。原因として多いのは、画面を長時間開いたままにしていた、途中でログインし直した、キャッシュが古い nonce を返している、アクション名や項目名が発行側と検証側で違う、の順です。自作コードなら、まず名前の一致を確認します。
まとめ
- nonce は CSRF を防ぐための「意図の確認」で、権限チェック(
current_user_can())・サニタイズ・エスケープの代わりにはならない - 既定では24時間程度有効で、期間内なら何度でも通る。アクション名に対象のIDを含めると流用を防げる
- フォームは
wp_nonce_field()とcheck_admin_referer()、保存処理はwp_verify_nonce()、リンクはwp_nonce_url()、Ajax はcheck_ajax_referer()を使う - REST API のCookie認証では
wp_restの nonce をX-WP-Nonceヘッダーで送り、permission_callbackで権限を確認する - ページキャッシュや未ログインの利用者では nonce の効果が変わるため、キャッシュ設定と他の対策を組み合わせる