【WordPress】Nonce(ノンス)の使い方とCSRF対策完全ガイド|フォーム・Ajax・REST APIの実装コード集

WordPressのテーマやプラグイン開発、あるいはカスタム機能の実装において、セキュリティ対策は最も重要視すべき要素のひとつです。

特にCSRF(クロスサイトリクエストフォージェリ)攻撃への対策として、WordPressではNonce(ノンス / ナンス)と呼ばれるセキュリティトークンを使用します。しかし、「とりあえず関数を使っているが仕組みを正しく理解できていない」「AjaxやREST APIでの正しい渡し方が分からない」「キャッシュプラグインと併用したらエラーが多発した」といった悩みを抱える開発者も少なくありません。

💡 本記事は「WordPress セキュアコーディング実践シリーズ」の第1弾(基礎・ハブ記事)です

本記事では、WordPressにおけるNonceの根本的な仕組みとCSRF対策の必要性を紐解き、フォーム送信(POST)・URLリンク(GET)・Ajax通信・REST APIの4大用途別における正しい実装コード(コピペ可)を徹底解説します。さらに、権限チェックとの併用やキャッシュ問題の回避策まで実務で役立つノウハウを完全網羅しました。


目次

WordPressにおけるNonce(ノンス)とは?仕組みとCSRF対策の必須性

Nonce(Number used once)は、暗号工学や情報セキュリティにおいて本来「一度だけ使われる乱数」を意味します。しかし、WordPressにおけるNonceは厳密な使い捨てトークンではなく、「特定のアクション・特定のユーザー・特定の時間枠」に紐づく暗号化ハッシュトークンとして設計されています。

1. Nonceが防ぐ「CSRF(クロスサイトリクエストフォージェリ)」の脅威

CSRF(Cross-Site Request Forgery)とは、Webサイトにログイン中のユーザー(特に管理者や編集者)のブラウザを悪用し、本人の意図しない悪意あるリクエストを送信させて不正な処理を実行させる攻撃です。

CSRF攻撃が発生する流れ

  1. 管理者がWordPress管理画面にログインする(ブラウザに認証Cookieが保持される)。
  2. 管理者が同一ブラウザの別タブ等で、攻撃者が用意した悪意ある罠サイト(またはフィッシングメールのリンク)を開く。
  3. 罠サイト内に仕込まれた非表示のフォームやスクリプトが、管理者のWordPressサイトへリクエスト(例: 記事削除や管理者権限ユーザー追加)を自動送信する。
  4. WordPressサーバーはブラウザから自動添付されたCookieを確認して「正規の管理者からの通信」と誤認し、悪意ある処理を実行してしまう。

WordPressの処理にNonce検証が組み込まれていれば、攻撃者は正規の画面から発行された正しいNonce値を知ることができないため、不正なリクエストはサーバー側で即座に遮断されます。

🚨 実際の脆弱性事例(CVE-2025-1463)
過去には有名プラグイン(Spreadsheet Integrationなど)において、管理処理のNonce検証が欠落していたために、管理者がリンクを踏むだけで意図しないデータ公開や設定変更が行われる重大なCSRF脆弱性が報告されました。安全な機能開発においてNonceの適切な実装は絶対条件です。

2. WordPressのNonceの有効期限(ライフサイクル)とカスタマイズ方法(nonce_lifeフィルター)

WordPressのNonceは、以下の4つの要素を組み合わせてハッシュ化(HMAC-MD5 / SHA-256ベース)して生成されます。

  • ユーザーID(未ログイン時は 0 またはセッショントークン)
  • アクション名(一意の操作識別文字列、例: 'delete_post_123'
  • 秘密鍵とソルトwp-config.php 内の NONCE_KEYNONCE_SALT
  • タイムウィンドウ(半日=12時間ごとの時間枠スロット)

WordPressのNonceはデフォルトで12〜24時間有効です。内部的には2つの12時間スロット(現在の12時間スロット+直前の12時間スロット)を受け付けるため、生成されたタイミングによって12時間から最大24時間有効となります。

wp_verify_nonce() 関数の戻り値は以下のようになっています:

  • 1直近0〜12時間以内に生成されたNonce(最新スロット)
  • 212〜24時間前に生成されたNonce(直前スロット)
  • false無効・期限切れ・改ざんされたNonce

有効期限をカスタマイズする(nonce_lifeフィルター)

セキュリティ要件が厳しいシステム等でNonceの有効期限を短縮したい場合や、逆に延長したい場合は、nonce_life フィルターフックを使用して秒単位で設定できます。

<?php
/**
 * Nonceの有効期限をデフォルトの24時間から4時間に短縮する
 * 
 * @param int $life 有効期限(秒)
 * @return int
 */
add_filter( 'nonce_life', function( $life ) {
    return 4 * HOUR_IN_SECONDS; // 4時間(14400秒)
} );

【用途別】WordPress Nonceの正しい実装コード集(コピペ可)

WordPress開発で頻出する4つの通信シーン(フォーム送信、URLリンク、Ajax通信、REST API)におけるNonceの実装パターンを整理しました。

1. フォーム送信(POST)でのNonce生成と検証(wp_nonce_field / check_admin_referer)

管理画面やフロントエンドのHTMLフォームからPOST送信を行う場合の最も標準的な実装です。

【HTMLフォーム側】wp_nonce_field で隠しフィールドを出力

<!-- 管理画面またはテーマのテンプレート内 -->
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <?php
    // 隠しフィールド (_wpnonce, _wp_http_referer) を自動出力
    // 第1引数: アクション名, 第2引数: フィールド名(省略時は '_wpnonce')
    wp_nonce_field( 'my_custom_post_action', 'my_custom_nonce' );
    ?>
    
    <input type="hidden" name="action" value="my_save_settings">
    
    <label for="site_notice">サイト告知テキスト:</label>
    <input type="text" id="site_notice" name="site_notice" value="" class="regular-text">
    
    <?php submit_button( '設定を保存' ); ?>
</form>

【PHPサーバー側】check_admin_referer による検証と権限チェック

<?php
// admin-post.php 経由のPOSTリクエストをフック
add_action( 'admin_post_my_save_settings', 'handle_my_save_settings' );

function handle_my_save_settings() {
    // 1. Nonce と リファラーの検証(不正な場合は自動で wp_die() 終了)
    check_admin_referer( 'my_custom_post_action', 'my_custom_nonce' );

    // 2. 権限チェック(認可)の実施
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( esc_html__( 'この操作を実行する権限がありません。', 'textdomain' ), 403 );
    }

    // 3. 入力値のサニタイズとデータ保存
    if ( isset( $_POST['site_notice'] ) ) {
        $notice = sanitize_text_field( wp_unslash( $_POST['site_notice'] ) );
        update_option( 'my_site_notice', $notice );
    }

    // 4. 処理完了後の安全なリダイレクト
    wp_safe_redirect( add_query_arg( 'updated', 'true', admin_url( 'admin.php?page=my-settings' ) ) );
    exit;
}

2. URLリンク(GET)でのNonce付与と検証(wp_nonce_url / wp_verify_nonce)

管理画面の一覧テーブルなどで「削除」「ステータス変更」リンクをGETパラメーター付きリンクで提供する場合の実装です。

【リンク生成側】wp_nonce_url でパラメータ付与

<?php
$item_id = 42;

// 基準となるURL
$base_url = admin_url( 'admin.php?page=my-items&action=delete_item&item_id=' . $item_id );

// URLに '_wpnonce' パラメータを自動付与
$secure_delete_url = wp_nonce_url( $base_url, 'delete_item_' . $item_id );
?>

<!-- 削除リンクの出力 -->
<a href="<?php echo esc_url( $secure_delete_url ); ?>" class="button delete-button" onclick="return confirm('本当に削除しますか?');">
    削除する
</a>

【PHPサーバー側】wp_verify_nonce による検証

<?php
add_action( 'admin_init', 'handle_my_item_delete' );

function handle_my_item_delete() {
    // 該当アクションのGETリクエストのみ処理
    if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'delete_item' ) {
        return;
    }

    $item_id = isset( $_GET['item_id'] ) ? absint( $_GET['item_id'] ) : 0;
    $nonce   = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';

    // 1. Nonceの検証
    if ( ! wp_verify_nonce( $nonce, 'delete_item_' . $item_id ) ) {
        wp_die( esc_html__( 'セキュリティチェックに失敗しました。', 'textdomain' ), 403 );
    }

    // 2. 権限チェック
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( esc_html__( '権限がありません。', 'textdomain' ), 403 );
    }

    // 3. 削除処理の実行
    // delete_my_item( $item_id );

    // 4. リダイレクト
    wp_safe_redirect( admin_url( 'admin.php?page=my-items&deleted=1' ) );
    exit;
}

3. Ajax通信でのNonceチェック(wp_create_nonce / check_ajax_referer)

admin-ajax.php を利用した非同期通信では、PHP側で生成したNonceを wp_localize_script() を使ってJavaScriptへ渡し、サーバー側のフックで check_ajax_referer() を使って検証します。

【PHP側】スクリプト登録とNonceの受け渡し

<?php
add_action( 'wp_enqueue_scripts', 'my_enqueue_ajax_scripts' );

function my_enqueue_ajax_scripts() {
    wp_enqueue_script(
        'my-ajax-script',
        get_template_directory_uri() . '/js/my-ajax.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );

    // JavaScriptにオブジェクト(URLとNonce)を渡す
    wp_localize_script(
        'my-ajax-script',
        'myAjaxObj',
        array(
            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( 'my_ajax_secret_action' ),
        )
    );
}

【JavaScript側】Ajaxリクエストの送信(fetch / jQuery)

// モダンな fetch API による実装例
document.querySelector('#my-ajax-btn')?.addEventListener('click', async () => {
    const formData = new FormData();
    formData.append('action', 'my_ajax_handler');
    formData.append('security', myAjaxObj.nonce); // PHPから渡されたNonce
    formData.append('post_id', 123);

    try {
        const response = await fetch(myAjaxObj.ajaxUrl, {
            method: 'POST',
            body: formData,
        });
        const result = await response.json();
        
        if (result.success) {
            console.log('成功:', result.data);
        } else {
            console.error('エラー:', result.data.message);
        }
    } catch (error) {
        console.error('通信エラー:', error);
    }
});

【PHPサーバー側】check_ajax_referer による検証

<?php
// ログインユーザー用
add_action( 'wp_ajax_my_ajax_handler', 'my_ajax_handler_function' );
// 未ログインユーザー用(必要な場合のみ)
// add_action( 'wp_ajax_nopriv_my_ajax_handler', 'my_ajax_handler_function' );

function my_ajax_handler_function() {
    // 1. Nonceの検証(失敗時は自動で 403 / JSONエラーレスポンスを返して終了)
    // 第1引数: アクション名, 第2引数: POSTキー名(デフォルトは '_ajax_nonce' または '_wpnonce')
    check_ajax_referer( 'my_ajax_secret_action', 'security' );

    // 2. 権限チェック
    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_send_json_error( array( 'message' => '権限が不足しています。' ), 403 );
    }

    $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;

    // 3. データ処理
    // ...

    wp_send_json_success( array(
        'message' => '処理が正常に完了しました。',
        'post_id' => $post_id,
    ) );
}

4. 【重要】WordPress REST APIでのNonce利用手順(X-WP-Nonceヘッダー)

WordPress REST API(/wp-json/...)は、Ajaxとは異なるNonce仕様を持っています。SPA(React / Vue)やブロックエディタ連携で必須となる知識です。

📌 REST API Nonce の3大原則
1. アクション名は常に 'wp_rest' 固定:独自のアクション名は使用しません。
2. リクエストヘッダー X-WP-Nonce で送信:HTTPヘッダーにセットして送信します。
3. 現在のログインユーザーの認証:Cookie認証と併用時、X-WP-Nonce があることで初めてWordPressはリクエストを「ログインユーザーの通信」として初期化します。

【JavaScript側】X-WP-Nonceヘッダーを付与して通信

// wp_localize_script 等で渡された wpApiSettings.nonce または wp_create_nonce('wp_rest') の値を使用
const restNonce = wpApiSettings.nonce; 

async function updateCustomPost(postId, data) {
    const response = await fetch(`/wp-json/my-plugin/v1/posts/${postId}`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-WP-Nonce': restNonce, // ★ここが最重要!
        },
        body: JSON.stringify(data),
    });

    if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
    }

    return await response.json();
}

【PHP側】REST APIルート登録と permission_callback の実装

<?php
add_action( 'rest_api_init', function() {
    register_rest_route( 'my-plugin/v1', '/posts/(?P<id>d+)', array(
        'methods'             => WP_REST_Server::EDITABLE, // POST, PUT, PATCH
        'callback'            => 'my_rest_update_post_handler',
        'permission_callback' => function( WP_REST_Request $request ) {
            // X-WP-Nonce によりログインユーザーが正しく初期化されているため、
            // current_user_can() で権限をチェックするだけで安全に認可できる
            $post_id = $request->get_param( 'id' );
            return current_user_can( 'edit_post', $post_id );
        },
        'args'                => array(
            'id' => array(
                'validate_callback' => function( $param ) {
                    return is_numeric( $param );
                }
            ),
        ),
    ) );
} );

function my_rest_update_post_handler( WP_REST_Request $request ) {
    $post_id = $request->get_param( 'id' );
    $params  = $request->get_json_params();

    // 更新処理
    // ...

    return rest_ensure_response( array(
        'success' => true,
        'post_id' => $post_id,
    ) );
}

主要Nonce関数のまとめ(クイックリファレンス)

関数名主な用途・説明戻り値 / 失敗時の挙動
wp_create_nonce($action)任意の $action 用にNonceトークン文字列(10文字ハッシュ)を生成文字列(Nonce)
wp_verify_nonce($nonce, $action)渡されたNonceの正当性と有効期限を検証1(0〜12h)、2(12〜24h)、false(無効)
wp_nonce_field($action, $name)フォーム用の <input type="hidden"> をHTML出力(または文字列返却)HTML文字列を出力(または返却)
wp_nonce_url($actionurl, $action, $name)指定URLに _wpnonce=... パラメータを付与したURL文字列を生成URL文字列
check_admin_referer($action, $name)管理画面POSTのNonceおよびリファラーを検証成功時は 1 または 2、失敗時は自動で wp_die() 終了
check_ajax_referer($action, $name, $die)AjaxリクエストのNonceを検証成功時は 1 または 2、失敗時は wp_die(-1) 終了($die=true時)

よくある実装ミスとセキュリティ上の注意点

Nonceを導入していても、設計や運用の落とし穴にはまるとセキュリティホールが発生したり、意図しない不具合の原因になります。

Nonceは認証・認可ではない(current_user_can との併用が必須)

開発初心者が最も陥りやすい誤解が、「Nonceチェックをパスしたから安全=実行して良い」と思い込んでしまうことです。

  • Nonceが証明すること:「このリクエストは、当サイトの正規の画面から送信されたものである(CSRFではない)」
  • Nonceが証明しないこと:「このリクエストを送信したユーザーに、該当データを変更・削除する権限があるかどうか(認可・アクセス制御)」

例えば、一般購読者(Subscriber)としてログインしているユーザーであっても、画面から正しいNonceを取得して管理者用エンドポイントにリクエストを送信することは可能です。current_user_can() による厳格な権限チェックを怠ると、特権昇格や不正アクセス(Broken Access Control)の脆弱性に直結します。

<?php
// ❌ 危険なコード:Nonceしかチェックしていない
function dangerous_delete_item() {
    check_ajax_referer( 'delete_item_action', 'security' );
    // 誰でも(一般会員でも)他人のデータを消せてしまう!
    delete_user_data( $_POST['item_id'] );
}

// ⭕ 安全なコード:Nonceと権限チェック(認可)を必ず併用
function safe_delete_item() {
    check_ajax_referer( 'delete_item_action', 'security' );
    
    // 権限・所有者チェック
    if ( ! current_user_can( 'delete_posts' ) ) {
        wp_send_json_error( array( 'message' => '権限がありません' ), 403 );
    }
    
    delete_user_data( $_POST['item_id'] );
}

キャッシュプラグイン併用時のNonce無効化問題と回避策

WP Super Cache、WP Rocket、LiteSpeed Cache、あるいは Cloudflare などのページキャッシュをフロントエンドに導入している場合、HTMLページ内のNonceがキャッシュされてしまい、12〜24時間経過後に「リンクの期限切れ」エラー(403 Forbidden)が発生する問題が頻発します。

キャッシュ問題の3大回避策

  1. NonceをAjax / REST APIで動的に注入する(推奨)
    ページHTML自体は静的キャッシュさせ、ページ読み込み後にJavaScriptから軽量なエンドポイント(Nonce取得API)を叩いて最新のNonceを取得・フォームに注入します。
  2. フォーム・動的ページをキャッシュ対象から除外する
    お問い合わせフォームや会員マイページなど、Nonceを多用する固定ページをキャッシュプラグインの設定で除外URIに登録します。
  3. ログインユーザーのキャッシュをバイパスする
    ログイン中ユーザーに対してはキャッシュを返さず、常に動的生成させる設定(大半のキャッシュプラグインで標準提供)を有効化します。

開発者として次のステップへ:セキュアコーディング体系の完成

WordPressでセキュアな機能を開発する際には、「CSRF対策(Nonce)」に加えて、「認可(権限チェック)」「入力サニタイズ」「出力エスケープ」の3大要素を組み合わせた包括的なセキュリティ設計が不可欠です。

本記事でNonceの実装と仕組みをマスターした後は、ぜひ以下のシリーズ関連記事に進み、セキュアなWordPress開発スキルを完成させましょう。

📚 WordPress セキュアコーディング実践シリーズ

参照リンク

🛡️ あなたのWordPressサイトの安全性をチェックしませんか?

プラグインの脆弱性や設定ミス、セキュリティリスクは目に見えない場所で発生します。「MozCheck」は、WordPressサイトのURLを入力するだけで、セキュリティ状態や改善ポイントを即座に診断できる無料ツールです。

投稿者

🧰 WordPress無料診断

サイト改善の第一歩をお届け

当サイト「MozCheck」は、WordPressサイトの不安をチェックできる無料診断サービスです。
URLを入力するだけ。登録不要、すぐに診断結果が表示されます。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です