Contents
Introduction
Nonces in WordPress are a lightweight CSRF protection mechanism used to validate that an action originated from the expected user and place in the UI. They are not cryptographically strong tokens for authentication, but when used correctly they prevent many accidental or scripted cross-site request forgery scenarios for logged-in users. This article explains, in exhaustive detail, how to generate and validate nonces in custom forms using PHP in WordPress, including admin forms, frontend forms, AJAX handlers, the REST API, and URLs. It includes code examples and best practices for robust, secure handling.
What is a WordPress nonce?
A WordPress nonce is a one-time-use token (actually time-limited, not strictly one-time) tied to an action name and a user/session. Nonces are designed to ensure a request to perform an action (for example: delete a post, update a setting) was initiated from an expected context (a form or link generated by your site) and not by a third-party site attempting to trigger that action on behalf of the user.
Key characteristics
- Time-limited: By default a nonce is valid for 24 hours (see Nonce lifetime below).
- Tied to a user/context: Nonces incorporate current user information (or 0 for logged-out users), and the supplied action string.
- Not a secret key: Nonces are not meant as authentication tokens or encryption keys.
How WordPress nonces work (brief technical)
WordPress generates a nonce by hashing together several pieces of information including the action string and a time-based “tick.” The lifetime constant NONCE_LIFE controls overall validity (default: 24 hours). Internally WP uses two ticks (current and previous) so a nonce remains valid across a grace period. Because nonces are predictable if you know the inputs, they are intended for CSRF protection rather than secrecy.
Nonce lifetime and ticks
- Default lifetime: 24 hours (filtered via the NONCE_LIFE filter).
- Internal tick length = NONCE_LIFE / 2 (e.g., 12 hours). A check accepts current and previous ticks, so the effective validity window is up to NONCE_LIFE seconds.
Core functions you need to know
wp_create_nonce(action) | Create a nonce string for a given action. |
wp_nonce_field(action, name = _wpnonce, referer = true, echo = true) | Print (or return) a hidden field with a nonce and optional referer field for forms. |
wp_nonce_url(url, action, name = _wpnonce) | Create a URL with a nonce parameter added. |
wp_verify_nonce(nonce, action) | Return 1/2/false depending on whether the nonce matches the generated token for current/previous tick or is invalid. |
check_admin_referer(action = -1, query_arg = _wpnonce) | Verify nonce and die (or return) for admin forms. Also checks referer if present. |
check_ajax_referer(action, query_arg = false, die = true) | Verify nonce for AJAX requests (works for logged-in / logged-out handlers) can die or return. |
Creating and validating nonces in custom forms (basic example)
Common pattern: generate the nonce in the form output using wp_nonce_field, then verify it in the handler using wp_verify_nonce (or use check_admin_referer if appropriate).
Form HTML output (server-side)
lt?php action = my_custom_form_action ?> ltform method=post action=> lt?php // Prints a hidden _wpnonce field and a _wp_http_referer field by default. wp_nonce_field( action, my_custom_nonce ) ?> ltinput type=hidden name=action value=my_custom_form_submit> ltinput type=text name=my_field value=> ltinput type=submit value=Submit> lt/form>
Server-side validation and processing
Hook a handler to admin-post.php or process in the same file. Example uses admin-post hooks which work for logged-in users (admin-post.php handles logged-in and logged-out via different action hooks).
lt?php // In plugin or theme functions.php add_action( admin_post_my_custom_form_submit, my_custom_form_submit_handler ) add_action( admin_post_nopriv_my_custom_form_submit, my_custom_form_submit_handler ) // for logged-out users function my_custom_form_submit_handler() { // 1) Check the nonce if ( ! isset( _POST[my_custom_nonce] ) ! wp_verify_nonce( _POST[my_custom_nonce], my_custom_form_action ) ) { // Nonce invalid: handle error, do not process wp_die( Security check failed, Error, array( response => 403 ) ) } // 2) Optional: verify capability or further checks if ( ! current_user_can( edit_posts ) ) { wp_die( Insufficient permissions, Error, array( response => 403 ) ) } // 3) Sanitize and process input my_field = isset( _POST[my_field] ) ? sanitize_text_field( wp_unslash( _POST[my_field] ) ) : // Do your processing here (update DB, etc.) // 4) Redirect after processing to avoid resubmission wp_safe_redirect( add_query_arg( my_status, success, wp_get_referer() ? wp_get_referer() : home_url() ) ) exit } ?>
Using check_admin_referer and check_ajax_referer
These convenience functions perform a nonce check and optionally die with a message. They also check the HTTP referer when appropriate. Use them when you want WP to handle failure responses automatically.
Example: check_admin_referer
// If the nonce fails, check_admin_referer will trigger wp_die() check_admin_referer( bulk-action, bulk_nonce ) // action and the nonce field name
Note: check_admin_referer is typically used for admin screens or forms that submit to admin-post.php. It will call wp_die() on failure unless you wrap it differently.
Example: check_ajax_referer for AJAX
// In the PHP AJAX handler: add_action( wp_ajax_my_ajax_action, my_ajax_handler ) add_action( wp_ajax_nopriv_my_ajax_action, my_ajax_handler ) function my_ajax_handler() { // This will die on failure with -1 by default (or JSON if appropriate). check_ajax_referer( my_ajax_nonce_action, security ) // Process AJAX request wp_send_json_success( array( message => OK ) ) }
On the client side you would include the nonce in the AJAX payload (or header). See the AJAX section below for a complete example.
Nonces in URLs
For actions triggered via links, use wp_nonce_url() to append a nonce parameter to a URL. When processing the action server-side, still use wp_verify_nonce or check_admin_referer to validate.
// Create URL url = wp_nonce_url( add_query_arg( array( action => my_link_action, post => post_id ), admin_url( admin.php ) ), delete_post_ . post_id, // action string my_link_nonce // query param name ) // Validate when handling the request: if ( ! isset( _GET[my_link_nonce] ) ! wp_verify_nonce( _GET[my_link_nonce], delete_post_ . post_id ) ) { wp_die( Invalid nonce ) }
AJAX: full example (enqueue, localize, JS and PHP)
Typical flow: generate nonce with wp_create_nonce and pass it to JS via wp_localize_script or wp_add_inline_script. Then include the nonce in the AJAX request payload or header and verify it in the PHP handler with check_ajax_referer.
PHP: enqueue and localize the nonce
add_action( wp_enqueue_scripts, my_enqueue_scripts ) function my_enqueue_scripts() { wp_enqueue_script( my-script, plugins_url( js/my-script.js, __FILE__ ), array( jquery ), 1.0, true ) // Create a nonce for AJAX action my_ajax_nonce_action nonce = wp_create_nonce( my_ajax_nonce_action ) // Provide nonce and ajax URL to JS wp_localize_script( my-script, MyScriptData, array( ajax_url => admin_url( admin-ajax.php ), nonce => nonce, ) ) }
JS: send AJAX request (using fetch)
// js/my-script.js function sendMyAjax() { var data = new FormData() data.append(action, my_ajax_action) // required for admin-ajax.php data.append(security, MyScriptData.nonce) // field name expected by check_ajax_referer data.append(my_field, value) fetch( MyScriptData.ajax_url, { method: POST, credentials: same-origin, body: data }) .then( response =gt response.json() ) .then( json =gt console.log( json ) ) .catch( err =gt console.error( err ) ) }
PHP: server-side AJAX handler
add_action( wp_ajax_my_ajax_action, my_ajax_handler ) add_action( wp_ajax_nopriv_my_ajax_action, my_ajax_handler ) function my_ajax_handler() { // Will die with -1 if nonce fails by default check_ajax_referer( my_ajax_nonce_action, security ) // Process request value = isset( _POST[my_field] ) ? sanitize_text_field( wp_unslash( _POST[my_field] ) ) : wp_send_json_success( array( received => value ) ) }
REST API and X-WP-Nonce
When calling WordPress REST endpoints that use cookie-based authentication, include the header X-WP-Nonce containing wp_create_nonce( wp_rest ). WordPress core checks that header and authenticates the user for cookie-based requests to the REST API. For custom REST routes, ensure your permission_callback uses current_user_can or relies on WP_REST authentication so the nonce is validated.
// Localize wpApiSettings.nonce (WP provides this for the REST API automatically in admin pages and via wp_enqueue_script in some contexts) // Example fetch call: fetch( /wp-json/my-plugin/v1/do-something, { method: POST, credentials: same-origin, headers: { Content-Type: application/json, X-WP-Nonce: MyScriptData.rest_nonce // set server side with wp_create_nonce(wp_rest) }, body: JSON.stringify( { foo: bar } ) } )
When registering REST routes, use permission_callback to check capabilities. WordPress core will check the nonce header for cookie-based authentication if present.
When to use which verification function
- wp_verify_nonce: Lightweight verification returns integer or false. Use when you want to handle failures yourself and do not want the function to die.
- check_admin_referer: Convenience wrapper for admin screens and forms it will verify the nonce and optionally the referer and die on failure.
- check_ajax_referer: Convenience wrapper tailored for AJAX requests (can die or return). Handles both logged-in and logged-out patterns.
Best practices and security considerations
- Always validate nonces server-side. Never rely on client-side checks alone.
- Use clear action strings. The action string should be descriptive and unique to the operation (e.g., myplugin_delete_item_123 when appropriate).
- Pair nonces with capability checks. A nonce validates the request origin, not user permissions. Call current_user_can() for sensitive operations.
- Sanitize all input. After verifying the nonce, sanitize and validate input before using it.
- Do not use nonces as auth tokens. Nonces are not authentication credentials and can be predictable for an attacker who can reproduce inputs.
- Avoid exposing nonces unnecessarily. Only include nonces in pages/forms where needed. Avoid leaking nonces in public logs or third-party resources.
- Use SSL/TLS (HTTPS). Protect your site with HTTPS to avoid referer leakage and MitM capturing of nonces or cookies.
- Use wp_safe_redirect and proper escaping. After form processing redirect to safe URLs and escape outputs with esc_url(), esc_attr(), etc.
- Be mindful for logged-out users. Nonces for unauthenticated users are possible (user = 0) but are less secure consider nonces plus server-side checks or alternate approaches for public actions.
Common pitfalls
- Wrong nonce field name or action string: The name parameter in wp_nonce_field must match what you check in wp_verify_nonce or check_admin_referer.
- Forgetting to call wp_nonce_field: If the field isn’t present the verification will fail.
- Using check_admin_referer on frontend AJAX incorrectly: Use check_ajax_referer for AJAX or ensure the context matches.
- Expecting nonces to be permanent: If a user opens a form and submits after nonce has expired, the check fails if you need longer validity consider adjusting NONCE_LIFE (carefully) or implement a server-side token stored in usermeta or option.
- Relying solely on nonces for authorization: Always perform capability checks.
Debugging tips
- Log the values you are verifying (careful with logs on production) to see what nonce was generated and what was submitted.
- Ensure the form posts to the same domain and uses correct action URLs (admin-post.php, admin-ajax.php, or a custom handler).
- Use browser devtools to confirm the nonce is actually present in the form payload or AJAX headers.
- Double-check you are not double-escaping or altering the nonce value before submission (e.g., using esc_html when not appropriate in value attributes).
Comprehensive example: admin settings form (generate, validate, save)
This example demonstrates a plugin settings form on an admin page using wp_nonce_field and check_admin_referer, plus capability checks, sanitization and redirection.
// Register admin menu and settings page add_action( admin_menu, myplugin_add_admin_menu ) function myplugin_add_admin_menu() { add_menu_page( My Plugin, My Plugin, manage_options, myplugin, myplugin_options_page ) } function myplugin_options_page() { // Output form. Action posts back to options.php or admin-post.php here we post to admin-post.php ?> ltdiv class=wrapgt lth1gtMy Plugin Settingslt/h1gt ltform method=post action=gt lt?php wp_nonce_field( myplugin_save_settings, myplugin_nonce ) ?gt ltinput type=hidden name=action value=myplugin_save_settings_actiongt lttable class=form-tablegt lttrgt ltthgtltlabel for=myplugin_optiongtOptionlt/labelgtlt/thgt lttdgtltinput name=myplugin_option id=myplugin_option type=text value=lt?php echo esc_attr( get_option( myplugin_option, ) ) ?gt /gtlt/tdgt lt/trgt lt/tablegt ltp class=submitgtltinput type=submit class=button-primary value=Save Changes /gtlt/pgt lt/formgt lt/divgt lt?php } // Handle form submission add_action( admin_post_myplugin_save_settings_action, myplugin_save_settings_action ) function myplugin_save_settings_action() { // Check user capability if ( ! current_user_can( manage_options ) ) { wp_die( Unauthorized, Error, array( response => 403 ) ) } // Verify nonce and referer (will die if invalid) check_admin_referer( myplugin_save_settings, myplugin_nonce ) // Sanitize input value = isset( _POST[myplugin_option] ) ? sanitize_text_field( wp_unslash( _POST[myplugin_option] ) ) : // Save option update_option( myplugin_option, value ) // Redirect back with success wp_safe_redirect( add_query_arg( updated, true, wp_get_referer() ? wp_get_referer() : admin_url( admin.php?page=myplugin ) ) ) exit }
When NONCE_LIFE might need to be adjusted (and caution)
You can change the effective nonce lifetime by filtering NONCE_LIFE. Doing so impacts security — shorter life improves CSRF resilience but can cause UX problems if users take too long to act longer life increases window for misuse. In most cases the default is appropriate.
// Example: set nonce life to 1 hour (3600 seconds) add_filter( nonce_life, function() { return 3600 } )
Summary checklist (quick)
- Generate nonces with wp_nonce_field or wp_create_nonce when rendering forms or supplying tokens to JS.
- Verify nonces server-side with wp_verify_nonce, check_admin_referer, or check_ajax_referer.
- Always combine nonce checks with capability checks (current_user_can).
- Sanitize all inputs and escape outputs.
- Use HTTPS, avoid exposing nonces unnecessarily, and be mindful of NONCE_LIFE.
Useful references
- WordPress Developer Handbook: Nonces
- wp_nonce_field()
- wp_verify_nonce()
- check_admin_referer()
- check_ajax_referer()
Final notes
WordPress nonces are a simple and effective CSRF mitigation when used properly. The most common validation errors stem from mismatched action strings or field names, missing fields, and expecting a nonce to be an authentication token. Follow the patterns shown here—generate on output, verify on input, sanitize, check capabilities, and always use HTTPS—to securely validate nonces in custom forms in PHP for WordPress.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |