2026年8月23日
2026年8月23日
WordPressのCapabilityチェックとNonceを組み合わせたセキュアなAJAX処理の書き方
WordPress の AJAX 処理を安全にするには、nonce で「本人が自分の画面から送ったリクエストか」を確かめ、current_user_can() で「その人にその操作をする権限があるか」を確かめる、という二つのチェックを両方行います。どちらか一方だけでは防げない攻撃があるためです。この記事では、ログインユーザーがフロントエンドから投稿の「確認済み」フラグを切り替える機能を例に、スクリプトの読み込みからハンドラー、エラーの返し方、テスト方法までを順に書きます。
nonce と capability はそれぞれ何を防ぐのか
nonce が防ぐのは CSRF(クロスサイトリクエストフォージェリ)です。 ログイン中のユーザーが悪意あるページを開いたとき、そのページから勝手に自サイトへリクエストを送らせる攻撃です。ブラウザはクッキーを自動で送るため、権限チェックだけでは「本人の操作」と見分けられません。WordPress の nonce はユーザー・ログインセッション・操作名(action)にひも付いた一時的な値で、正規の画面にしか埋め込まれないため、これを確認することで外部ページからのなりすましを防げます。
capability が防ぐのは権限の飛び越えです。 nonce は「誰が送ったか」を確かめるだけで、「その人に許されている操作か」は判定しません。購読者が自分の画面で正規の nonce を取得し、他人の投稿IDを指定して送ってくる可能性もあります。そこで current_user_can() で権限を確認します。
つまり nonce は認可(権限)の代わりにならず、権限チェックは CSRF 対策の代わりになりません。両方が必要です。
nonce の有効期間は既定で最大24時間程度で、nonce_life フィルターで変更できます。厳密な「使い捨て」ではない点も覚えておいてください。
全体の流れ
- スクリプトを読み込み、AJAX の送信先URLと nonce を JavaScript に渡す
- ボタンのクリックで
admin-ajax.phpに POST する wp_ajax_{action}フックのハンドラーで、nonce → 入力値の検証 → 権限の順に確認するwp_send_json_success()/wp_send_json_error()で結果を返す- JavaScript で結果を
textContentで画面に反映する
コードは子テーマの functions.php か独自プラグインに置きます。変更前にバックアップを取り、ステージングで試してから本番に反映してください。
手順1:スクリプトを読み込み nonce を渡す
nonce は wp_create_nonce() で作ります。引数の操作名は、あとでハンドラー側の確認に使う文字列と一致させます。
<?php
add_action( 'wp_enqueue_scripts', 'myprefix_enqueue_review_script' );
function myprefix_enqueue_review_script() {
if ( ! is_singular( 'post' ) || ! current_user_can( 'edit_post', get_queried_object_id() ) ) {
return;
}
wp_enqueue_script(
'myprefix-review',
get_stylesheet_directory_uri() . '/js/review-toggle.js',
array(),
'1.0.0',
array(
'in_footer' => true,
'strategy' => 'defer',
)
);
$config = array(
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'myprefix_toggle_reviewed' ),
);
wp_add_inline_script(
'myprefix-review',
'var myprefixAjax = ' . wp_json_encode( $config ) . ';',
'before'
);
}
編集権限のないユーザーにはスクリプト自体を読み込ませていません。これは表示を整えるための配慮で、セキュリティ対策の本体はあくまでサーバー側のチェックです。wp_enqueue_script() の第5引数に配列で strategy を渡す書き方は WordPress 6.3 以降で使えます。
ボタンはテンプレート(single.php など)に出力します。
<?php if ( current_user_can( 'edit_post', get_the_ID() ) ) : ?>
<p class="myprefix-review">
<button type="button" class="myprefix-review-toggle" data-post-id="<?php echo esc_attr( get_the_ID() ); ?>">確認済みを切り替え</button>
<span class="myprefix-review-status" aria-live="polite"></span>
</p>
<?php endif; ?>
手順2:JavaScript から送信する
document.querySelectorAll( '.myprefix-review-toggle' ).forEach( ( button ) => {
button.addEventListener( 'click', async () => {
const status = button.parentElement.querySelector( '.myprefix-review-status' );
const body = new URLSearchParams( {
action: 'myprefix_toggle_reviewed',
nonce: myprefixAjax.nonce,
post_id: button.dataset.postId,
} );
button.disabled = true;
try {
const response = await fetch( myprefixAjax.url, {
method: 'POST',
credentials: 'same-origin',
body,
} );
const json = await response.json();
status.textContent = json.success ? json.data.label : json.data.message;
} catch ( error ) {
status.textContent = '通信に失敗しました。時間をおいて再度お試しください。';
} finally {
button.disabled = false;
}
} );
} );
action の値が、PHP 側の wp_ajax_ に続く名前になります。サーバーから返ったメッセージは innerHTML ではなく textContent で表示します。返ってくる値に HTML が混ざっていても、文字として表示されるだけでスクリプトは動きません。
手順3:ハンドラーで nonce・入力・権限を確認する
<?php
add_action( 'wp_ajax_myprefix_toggle_reviewed', 'myprefix_ajax_toggle_reviewed' );
// ログインしていないユーザーには使わせないので wp_ajax_nopriv_ は登録しない
function myprefix_ajax_toggle_reviewed() {
// 1. nonce:本人が正規の画面から送ったか
if ( ! check_ajax_referer( 'myprefix_toggle_reviewed', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => '画面の有効期限が切れました。再読み込みしてください。' ), 403 );
}
// 2. 入力値:整数に変換し、対象が存在するか確かめる
$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
$post = $post_id ? get_post( $post_id ) : null;
if ( ! $post || 'post' !== $post->post_type ) {
wp_send_json_error( array( 'message' => '対象の投稿が見つかりません。' ), 404 );
}
// 3. capability:この投稿を編集できるか(投稿ID付きで判定)
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_send_json_error( array( 'message' => 'この投稿を変更する権限がありません。' ), 403 );
}
$reviewed = ! (bool) get_post_meta( $post_id, '_myprefix_reviewed', true );
update_post_meta( $post_id, '_myprefix_reviewed', $reviewed ? 1 : 0 );
wp_send_json_success(
array(
'reviewed' => $reviewed,
'label' => $reviewed ? '確認済み' : '未確認',
)
);
}
ポイントを補足します。
check_ajax_referer()の第3引数を false にすると、失敗時に処理を止めずに false を返します。既定(true)のままだと-1という文字列を返して終了するため、JSON を期待している JavaScript 側でエラーになります。自分で JSON のエラーを返したいときは false にします。- 権限は
edit_posts(投稿全般を編集できるか)ではなく、edit_postに投稿IDを付けて判定しています。後者は「他人の投稿は編集できない投稿者」のようなロールの違いを正しく反映します。クライアントから届いたIDは書き換えられる前提で、対象ごとに判定するのが大切です。 wp_send_json_success()とwp_send_json_error()は、JSON を出力したあと処理を終了します。第2引数の HTTP ステータスコードは WordPress 4.7 以降で指定できます。- メタキーの先頭を
_にしているのは、管理画面のカスタムフィールド欄に表示させないためです。
よくある間違い
**is_admin() を権限チェックに使う。** admin-ajax.php へのリクエストでは is_admin() が常に true になります。これは「管理画面のURLかどうか」を返す関数で、管理者かどうかの判定ではありません。
nonce の操作名が一致していない。 wp_create_nonce( 'save_note' ) と check_ajax_referer( 'save-note', ... ) のように一文字でも違うと失敗します。定数にしておくと防げます。
nonce だけ確認して権限を見ない/権限だけ見て nonce を確認しない。 冒頭で説明したとおり、防げる攻撃が違うので両方必要です。
ログアウト中のユーザー向け処理を nonce で守れると考える。 wp_ajax_nopriv_ で受ける処理の nonce は、ログアウト中のユーザー全員に共通の値になるため、なりすまし対策としての効果は限られます。お問い合わせやいいねボタンのように未ログインで使わせる機能では、入力値の厳しい検証、送信回数の制限(IPごとに transient で回数を数えるなど)、書き込む内容を最小限にすることで守ります。また、ページキャッシュを使っているサイトでは、HTML に埋め込んだ nonce が古いまま配信されて期限切れになりやすい点にも注意が必要です。
動作を確認する
ブラウザの開発者ツールの「ネットワーク」タブで、admin-ajax.php へのリクエストとレスポンスを見ながら、次の四通りを試します。
- 編集権限のあるユーザーで操作する →
success: trueが返る - 購読者でログインし、開発者ツールから他人の投稿IDで送る → 403 と権限エラーのメッセージ
- nonce の値を書き換えて送る → 403 と有効期限のメッセージ
- ログアウト状態で
curlから送る →0が返る(ハンドラーが登録されていない扱い。多くのバージョンでステータスは400)
curl -s -i -X POST https://example.com/wp-admin/admin-ajax.php \
-d "action=myprefix_toggle_reviewed&post_id=1&nonce=invalid"
テストは自分が管理するサイトのステージング環境で行ってください。
REST API で書く場合
新しく作る機能なら、AJAX ではなく REST API のカスタムエンドポイントで実装する選択肢もあります。register_rest_route() の permission_callback に権限チェックを書き、JavaScript からは wp_create_nonce( 'wp_rest' ) で作った nonce を X-WP-Nonce ヘッダーで送ります。permission_callback を省略すると WordPress 5.5 以降では注意メッセージが出るため、権限の確認漏れに気づきやすい仕組みになっています。考え方は同じで、「CSRF 対策(nonce)」と「権限確認」を別々に行います(2026年10月現在)。
まとめ
- nonce は CSRF 対策、
current_user_can()は権限確認。役割が違うので AJAX 処理では両方を行う。 - nonce の操作名は作成側と確認側で一致させ、
check_ajax_referer( ..., false )で JSON のエラーを返すと扱いやすい。 - 権限は
edit_postのように対象IDを付けて判定し、クライアントから届いたIDは信用しない。 - 入力は
absint()などで整え、出力はtextContentやエスケープ関数で無害化する。 - 未ログイン向けの処理は nonce に頼らず、検証と回数制限で守る。