WordPress Nonce Example: Protect Admin Forms and AJAX Actions From CSRF
Practical WordPress nonce example for admin forms and AJAX actions, including CSRF protection, check_ajax_referer, capability checks, and admin-ajax.php -1 failures.
Published
April 16, 2026
Reading Time
4 min read
Updated
May 4, 2026

Hardening Notes
Baselines, access reduction, and default settings that stand up in production.
Best For
Teams preparing, launching, or maintaining WordPress as a backend service in a production stack.
Primary Topics
Editorial Focus
Control Ledger: Baselines, access reduction, and default settings that stand up in production. Updated on May 4, 2026.
Full Report
Last reviewed: April 16, 2026
WordPress nonce examples are searched because developers eventually build a form, AJAX action, admin tool, or REST-powered feature that changes data. The dangerous shortcut is assuming that “logged in” is enough. For state-changing actions, you also need protection against cross-site request forgery.
This guide explains how to use WordPress nonces in admin forms and AJAX actions, what nonces do, what they do not do, and how to combine them with capability checks. A nonce is not authentication. It is one control in a complete request validation path.
Why admin-ajax.php returns -1 with a WordPress nonce
admin-ajax.php often returns -1 when WordPress rejects a nonce check before your callback sends a custom JSON response. The usual causes are a missing nonce field, a nonce created with a different action string, an expired nonce from an old admin tab, sending the token under the wrong request key, or calling the AJAX action as a user who cannot access the screen that generated the nonce.
When debugging this, compare the action passed to wp_create_nonce() with the action passed to check_ajax_referer(), confirm the JavaScript sends the same request key, and test the failure path without changing data. If the action also needs authorization, keep the current_user_can() check separate so a valid nonce never becomes a permission grant.
Who this is for
This article is for plugin developers, theme developers, and operators reviewing custom WordPress code. If your code deletes data, saves settings, triggers imports, changes users, starts maintenance jobs, or updates options, nonce verification should be part of the review.
What a WordPress nonce protects
A nonce helps verify that a request came from a screen or flow WordPress intentionally generated for the current user. It is primarily a CSRF protection mechanism. If an attacker tricks a logged-in administrator into visiting a malicious page, the malicious page should not be able to submit a valid state-changing request to your WordPress admin action.
A nonce does not replace authentication, authorization, input validation, escaping, logging, or rate limiting. It answers one question: did this request include a valid token for the expected action?
Admin form example
The example below renders a small admin form and verifies the nonce before saving an option. It also checks capabilities before rendering and before processing. That duplication is intentional: never rely on UI visibility as access control.
add_action( 'admin_menu', 'vulnwp_register_settings_page' );
function vulnwp_register_settings_page() {
add_options_page(
'VulnWP Example Settings',
'VulnWP Example',
'manage_options',
'vulnwp-example',
'vulnwp_render_settings_page'
);
}
function vulnwp_render_settings_page() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to access this page.', 'vulnwp' ) );
}
if ( isset( $_POST['vulnwp_example_submit'] ) ) {
vulnwp_handle_settings_save();
}
$value = get_option( 'vulnwp_example_message', '' );
?>
<div class="wrap">
<h1><?php esc_html_e( 'VulnWP Example Settings', 'vulnwp' ); ?></h1>
<form method="post">
<?php wp_nonce_field( 'vulnwp_save_settings', 'vulnwp_settings_nonce' ); ?>
<label for="vulnwp_example_message">
<?php esc_html_e( 'Message', 'vulnwp' ); ?>
</label>
<input
type="text"
id="vulnwp_example_message"
name="vulnwp_example_message"
value="<?php echo esc_attr( $value ); ?>"
class="regular-text"
/>
<?php submit_button( 'Save', 'primary', 'vulnwp_example_submit' ); ?>
</form>
</div>
<?php
}
function vulnwp_handle_settings_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to save these settings.', 'vulnwp' ) );
}
check_admin_referer( 'vulnwp_save_settings', 'vulnwp_settings_nonce' );
$message = isset( $_POST['vulnwp_example_message'] )
? sanitize_text_field( wp_unslash( $_POST['vulnwp_example_message'] ) )
: '';
update_option( 'vulnwp_example_message', $message );
}
Why this works
wp_nonce_field() prints the hidden token. The form includes a nonce tied to the action string vulnwp_save_settings.
check_admin_referer() verifies the submitted token. If the token is missing or invalid, WordPress stops the request instead of saving the option.
current_user_can() handles authorization. The nonce proves request intent. The capability check proves the current user is allowed to do the action.
Input is still sanitized. Nonce verification does not make submitted input safe. The posted message is unslashed and sanitized before storage.
AJAX action example
For admin AJAX, create a nonce in the page and verify it inside the AJAX callback.
add_action( 'admin_enqueue_scripts', 'vulnwp_admin_assets' );
function vulnwp_admin_assets( $hook ) {
if ( 'settings_page_vulnwp-example' !== $hook ) {
return;
}
wp_enqueue_script(
'vulnwp-admin',
plugin_dir_url( __FILE__ ) . 'admin.js',
array(),
'1.0.0',
array( 'in_footer' => true )
);
wp_add_inline_script(
'vulnwp-admin',
'window.vulnwpAdmin = ' . wp_json_encode(
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'vulnwp_run_check' ),
)
) . ';',
'before'
);
}
add_action( 'wp_ajax_vulnwp_run_check', 'vulnwp_ajax_run_check' );
function vulnwp_ajax_run_check() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Forbidden.' ), 403 );
}
check_ajax_referer( 'vulnwp_run_check', 'nonce' );
wp_send_json_success(
array(
'status' => 'ready',
)
);
}
The JavaScript would send action=vulnwp_run_check and nonce=window.vulnwpAdmin.nonce to admin-ajax.php. The server still makes the final decision.
Production checklist
- Use a unique action string for each meaningful action.
- Verify the nonce before performing state-changing work.
- Keep capability checks separate from nonce checks.
- Sanitize and validate input even after nonce verification.
- Do not expose sensitive secrets in inline JavaScript.
- Return clear error responses for AJAX and REST workflows.
- Test missing nonce, invalid nonce, low-privilege user, and valid admin flows.
Common mistakes
- Treating nonces as passwords. They are not secrets for long-term authentication.
- Using one generic nonce for everything. Action-specific nonces are easier to reason about.
- Skipping capability checks. A valid nonce from a low-privilege user should not grant admin power.
- Verifying only in JavaScript. Browser checks are not security controls.
- Saving raw
$_POSTvalues. Nonce verification does not sanitize input.
Where this fits in a hardening review
Nonce review is especially important when auditing custom plugins and admin tools after an incident or before a launch. If a plugin adds admin forms, import actions, AJAX endpoints, or REST writes, verify CSRF protection before trusting it. Pair this with our admin AJAX secure action example, REST API exposure checklist, and administrator access audit checklist.
Failure cases to test
A nonce implementation is not verified until the failure paths are tested. Test the action with no nonce, an invalid nonce, a valid nonce from a low-privilege user, and a valid nonce from the intended user. The first three should fail before state changes happen. The last one should succeed only after input validation passes.
# Missing nonce should fail.
curl -i -X POST https://example.com/wp-admin/admin-ajax.php
-d "action=vulnwp_run_check"
# Invalid nonce should fail.
curl -i -X POST https://example.com/wp-admin/admin-ajax.php
-d "action=vulnwp_run_check&nonce=invalid"
For browser-based admin tools, also test a stale tab. Nonces are time-limited, so long-lived admin screens should handle expired tokens with a clear error or refresh path. A silent failure creates support noise, while automatically retrying privileged actions without user intent can create its own risk.
REST API note
For cookie-authenticated REST requests from the WordPress admin, WordPress commonly uses the wp_rest nonce. That still does not remove the need for permission_callback. The REST route should verify that the user can perform the action, and the callback should sanitize the request payload before using it.


