Independent Editorial DeskWordPress Releases, Builds, and Operations
Back to Archive
Hardening NotesImplementation Notes

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

Abstract token verification symbol representing WordPress nonce and CSRF protection.
Control LedgerHardening Notes

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

Hardening NotesImplementation Notes

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 $_POST values. 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.

References and further reading

Popular Guides

Popular WordPress guides to read next.

These articles connect recurring production concerns: implementation details, updates, troubleshooting, recovery paths, and operational cleanup.

Continue Reading

More from the archive.

Diagnostic dashboard scene representing a WordPress Site Health review before major updates.
01Build Pattern
Implementation Notes

Build Pattern

Extension points, code paths, and implementation choices that should survive contact with production.

May 21, 2026 · 3 min read

WordPress Site Health Check Before Major Updates: What to Review First

A pre-update WordPress Site Health checklist covering loopbacks, connectivity, debug settings, and environment readiness.

Structured data and route review scene representing permalink validation after a WordPress migration.
02Build Pattern
Implementation Notes

Build Pattern

Extension points, code paths, and implementation choices that should survive contact with production.

May 21, 2026 · 3 min read

WordPress Permalink Checklist After Migration: Catch URL Problems Early

A post-migration WordPress permalink checklist for checking rewrite rules, post URLs, archives, and redirect noise.

Technical media workspace representing image preparation and optimization before upload to WordPress.
03Build Pattern
Implementation Notes

Build Pattern

Extension points, code paths, and implementation choices that should survive contact with production.

May 21, 2026 · 3 min read

WordPress Image Optimization Checklist: What to Fix Before Upload

A practical WordPress image optimization checklist covering dimensions, compression, formats, and Media settings before upload.