WordPress Nonce System: Implementation and CSRF Protection Best Practices

WordPress nonces (“numbers used once”) are cryptographic tokens that protect forms and URL-based actions against Cross-Site Request Forgery (CSRF) attacks — when a nonce is required, an attacker cannot forge a valid request on behalf of a logged-in user because the attacker’s page has no way to obtain the current nonce value for that user, site, and action combination. A WordPress nonce is generated by wp_create_nonce( 'action-name' ) and verified by wp_verify_nonce( $nonce_value, 'action-name' ) — the return value is 1 if the nonce is valid and less than 12 hours old, 2 if it is valid but more than 12 hours old (the second half of the 24-hour nonce lifespan), and false if invalid. WordPress nonces are not true cryptographic one-time tokens — they are valid for 24 hours and can be reused within that window — but they include the user ID, session token (from wp_get_session_token()), and a timestamp-based tick in the HMAC-SHA256 hash, making them user-specific and session-bound. Forms use wp_nonce_field( 'my-action', '_wpnonce' ) which outputs a hidden input field, while URL-based actions use wp_nonce_url( $url, 'my-action' ) which appends _wpnonce as a query parameter. AJAX requests in WordPress should pass the nonce in the request data and verify it in the AJAX handler with check_ajax_referer( 'my-action', 'nonce_field_name' ) — this function calls wp_die() automatically on failure, cleanly aborting the handler. REST API endpoints use wp_rest_request_nonce() and the X-WP-Nonce header pattern — the REST API infrastructure validates this header automatically for authenticated requests. Nonce lifespan can be extended or shortened via the nonce_life filter (return value in seconds) — a 4-hour lifespan is appropriate for sensitive admin actions, a 12-hour lifespan for front-end forms where users may leave tabs open. The SQL injection/XSS post covers input sanitization; nonces cover the CSRF layer of the WordPress security model.

Problem: A WordPress plugin adds a “Delete all user data” button to the admin profile page — clicking it sends a POST request to an admin-ajax.php handler that deletes the user’s posts and meta. An attacker embeds an auto-submitting form on their site that targets the same endpoint — any admin who visits the attacker’s page while logged in silently loses their data.

Solution: Add a nonce to the form and the AJAX handler, verify the nonce before executing any data modification, and check the current user’s capability — three lines of security code that block the CSRF attack entirely.

// ── 1. Output form with nonce field ──────────────────────────────────────
add_action( 'show_user_profile', 'myplugin_render_delete_button' );
add_action( 'edit_user_profile', 'myplugin_render_delete_button' );

function myplugin_render_delete_button( WP_User $user ): void {
    // Only show to the user themselves or admins
    if ( ! current_user_can( 'edit_user', $user->ID ) ) return;
    ?>
    
ID, '_myplugin_nonce' ); ?>
\d+)', [ 'methods' => 'DELETE', 'permission_callback' => fn( WP_REST_Request $r ) => current_user_can( 'edit_user', (int) $r['id'] ), 'callback' => fn( WP_REST_Request $r ) => myplugin_delete_all_user_data( (int) $r['id'] ) ? new WP_REST_Response( null, 204 ) : new WP_Error( 'delete_failed', 'Deletion failed', [ 'status' => 500 ] ), ] ); } ); // REST API automatically validates X-WP-Nonce header for logged-in users; // use wp_create_nonce('wp_rest') in the frontend JS and pass it as X-WP-Nonce.

NOTE: check_ajax_referer() and wp_verify_nonce() are not equivalent — check_ajax_referer() also checks the HTTP Referer header against allowed origins and calls wp_die() on failure, making it more suitable for AJAX handlers. For non-AJAX contexts where you want to handle the failure gracefully rather than dying, use wp_verify_nonce() and handle the false return yourself. Never use nonces as authentication tokens — they only verify that the request originated from a form rendered by your site for a specific logged-in user; they do not replace capability checks.