Glossary

What are Nonces in WordPress? Security Tokens explained

August 5, 2026

A nonce in WordPress is a security token that gets attached to forms, links and AJAX requests so that WordPress can verify the request was intentionally triggered by a specific logged-in user. The name comes from cryptography and stands for number used once, but that is slightly misleading here: a WordPress nonce is not a number, is not used only once and is technically a hash tied to the user, the session and one specific action. It stays valid for up to 24 hours. The main threat nonces defend against is cross-site request forgery (CSRF), an attack where a malicious page silently submits a request with your logged-in session, for example to delete a post or change a setting. Nonces work, which is exactly why the most common security mistake around them is on the other side: a nonce proves intent, not permission. Plugin code that checks a nonce but forgets the capability check with current_user_can() is one of the most frequent sources of published WordPress vulnerabilities.

What is a nonce? The name is slightly misleading

In cryptography, a nonce is a value that is used exactly once and never again, which is what makes it useful against replay attacks. WordPress borrowed the name but not the strict definition. A WordPress nonce is a short hash generated from four ingredients: the action it belongs to, the ID and session token of the current user, the current time window and the secret keys from your wp-config.php. That construction has three practical consequences:

  • A nonce is personal. A token generated for one user fails validation for every other user, so it cannot be shared or stolen from a public page.
  • A nonce is action-specific. A token created for the action delete-post_42 is worthless for delete-post_43 or for any other operation. That is why hardcoding a generic action name weakens the whole mechanism.
  • A nonce is reusable within its lifetime. The same token validates as often as you like for up to 24 hours. WordPress explicitly documents this, so a nonce alone does not protect against replaying a request the attacker has already captured.

The official WordPress developer documentation is refreshingly honest about all of this and describes nonces as one layer of defence, not a complete security system.

What attacks do WordPress nonces prevent? CSRF explained

The attack nonces exist for is cross-site request forgery. The scenario: you are logged in to your WordPress admin in one browser tab. In another tab you open a completely unrelated page that an attacker controls. That page contains a hidden form or image tag pointing at your site, something like a request to deactivate a plugin or create a user. Because your browser automatically sends your WordPress cookies with every request to your domain, the request arrives fully authenticated, and WordPress would happily execute it. You clicked nothing, you saw nothing, and your site just gained a new administrator.

Nonces break this attack at the last step. The malicious page can forge the request, but it cannot know the current nonce for your user and that action, because the token only appears inside pages WordPress renders for you. The forged request arrives without a valid nonce, verification fails, and WordPress stops with the famous white screen that asks: Are you sure you want to do this?

How long is a WordPress nonce valid? The tick system

WordPress divides time into ticks of twelve hours. When a nonce is verified, WordPress accepts tokens from the current tick and the previous one, which is where the maximum lifetime of 24 hours comes from. The verification functions even tell you which half you are in: they return 1 if the nonce was generated within the last twelve hours and 2 if it is between twelve and 24 hours old, and false if it is invalid or expired.

The lifetime can be changed with the nonce_life filter, which some security-focused setups use to shorten the window:

// Shorten nonce lifetime to 4 hours
add_filter( 'nonce_life', function () {
	return 4 * HOUR_IN_SECONDS;
} );

Shorter lifetimes shrink the replay window but also make cached pages with embedded nonces expire faster, which is a real problem on sites with aggressive page caching. If a cached page serves a nonce that is older than 24 hours, every visitor gets a token that can never validate. This is the classic reason why AJAX features on heavily cached WordPress sites mysteriously fail for some visitors.

How to create a nonce in WordPress

WordPress ships three helper functions for generating nonces, one for each place a token typically lives:

// 1. In a form: adds a hidden input field plus a referer field
wp_nonce_field( 'save_settings_action', 'my_plugin_nonce' );

// 2. In a URL: appends ?_wpnonce=... to a link
$url = wp_nonce_url( admin_url( 'admin.php?page=my-plugin&delete=42' ), 'delete-item_42' );

// 3. Raw token, e.g. for passing to JavaScript
$nonce = wp_create_nonce( 'my_ajax_action' );

The first argument is always the action, and this is where care pays off. An action string like delete-item_42 that includes the object ID produces a token that only works for exactly that item. A lazy generic action like my_nonce shared across a whole plugin means one leaked or predictable-context token unlocks every operation the plugin offers.

How to verify a nonce: wp_verify_nonce, check_admin_referer and check_ajax_referer

Verification has one low-level function and two convenience wrappers that you will meet far more often in real code:

// Low level: returns 1, 2 or false, never dies
if ( ! wp_verify_nonce( $_POST['my_plugin_nonce'], 'save_settings_action' ) ) {
	return; // stop, invalid request
}

// Admin screens: verifies and dies with an error page on failure
check_admin_referer( 'save_settings_action', 'my_plugin_nonce' );

// AJAX handlers: same idea for admin-ajax.php requests
check_ajax_referer( 'my_ajax_action', 'security' );

The wrappers stop execution on failure, which is usually what you want at the top of a handler. The low-level wp_verify_nonce() gives you the return value instead, so you can respond with a proper JSON error in an API context. Whichever variant you use, the verification must sit before the action is performed, not after, which sounds obvious and is still a recurring bug in plugin changelogs.

Nonces in the REST API and AJAX requests

The WordPress REST API uses the same mechanism with a fixed action name. For any request that relies on cookie authentication, WordPress expects a nonce created for the action wp_rest, delivered in the X-WP-Nonce request header. Scripts registered the standard way get this automatically: wp_localize_script() or the newer wp.apiFetch bundle pass the token along, and every logged-in REST request carries it without the developer thinking about it.

// Passing a REST nonce to your JavaScript
wp_localize_script( 'my-app', 'myAppData', array(
	'restUrl' => esc_url_raw( rest_url( 'my-plugin/v1/' ) ),
	'nonce'   => wp_create_nonce( 'wp_rest' ),
) );

// In JavaScript: send it as a header
fetch( myAppData.restUrl + 'items', {
	method: 'POST',
	headers: { 'X-WP-Nonce': myAppData.nonce, 'Content-Type': 'application/json' },
	body: JSON.stringify( { title: 'New item' } ),
} );

Without the header, the REST API treats the request as unauthenticated, even though the browser sent valid login cookies. That is deliberate: it is exactly the CSRF protection described above, applied to the API.

Why a nonce is not a permission check

This is the single most important thing to understand about nonces, and it is the part that regularly goes wrong in the plugin ecosystem. A successful nonce check proves two things: the request originated from a page WordPress generated, and it belongs to the user it was issued to. It does not prove that this user is allowed to do what the request asks for. A subscriber can have a perfectly valid nonce, because WordPress happily issues nonces to every logged-in user, and some tokens even work for visitors who are not logged in at all.

Correct handler code therefore always asks two separate questions:

// Intent: is this a genuine request from this user?
check_admin_referer( 'save_settings_action', 'my_plugin_nonce' );

// Permission: is this user allowed to do this?
if ( ! current_user_can( 'manage_options' ) ) {
	wp_die( 'Insufficient permissions.' );
}

Vulnerability databases are full of what happens when the second question is skipped. Broken access control and CSRF consistently rank among the most common categories in WordPress plugin CVEs, and a large share of those entries boil down to a handler that checked a nonce, or nothing at all, instead of a capability. If you read a plugin advisory that says any authenticated user could change settings or missing authorization, this pattern is usually behind it.

Common nonce mistakes in themes and plugins

  • Nonce check instead of capability check. The classic, described above. Both checks are needed, they answer different questions.
  • One generic nonce for everything. A single action string across a whole plugin turns a narrow token into a master key.
  • Nonces in cached pages. Full-page caches serve tokens beyond their 24 hour lifetime, silently breaking forms and AJAX for some visitors. Fragment caching or fetching the nonce separately solves it.
  • Verifying after acting. The check must be the first thing the handler does.
  • Trusting a nonce as proof of identity for logged-out users. Nonces for visitors without a session are weaker by construction, since the user component of the hash is empty.
  • Leaking nonces in URLs. Tokens appended to links end up in server logs, browser history and the Referer header. For destructive actions, POST requests with wp_nonce_field() are the better home.

How InspectWP helps you spot the consequences

You cannot see from the outside whether a plugin verifies its nonces correctly, but you can see the consequences once they become public. InspectWP detects the plugins and themes of an analysed WordPress site along with their versions and matches them against known vulnerability data, and CSRF and missing-authorization flaws of exactly the kind described in this article make up a large share of those entries. If a report flags an installed plugin with a published vulnerability, the fix is almost always the same and refreshingly simple: update. With scheduled automatic reports, that loop of detect, learn and update runs continuously instead of whenever someone happens to think of it.

Check your WordPress site now

InspectWP analyzes your WordPress site for security issues, SEO problems, GDPR compliance, and performance โ€” for free.

Analyze your site free