Skip to content

WordPress Nonce: What It Is and How to Use It Correctly

A WordPress nonce is a short-lived token that proves a request came from your own page. Here is how to create and verify one, what it does not protect, and why caching breaks it.

Stack Mode Team 11 min read

If you have written more than one WordPress admin form, you have met the WordPress nonce. It is the strange hidden field and the check_admin_referer() line that everyone copies and nobody quite explains. This article explains it in plain words, shows the PHP and JavaScript to use it properly, and covers the mistakes that turn a small security helper into a false sense of safety.

It is aimed at plugin and theme developers, but you do not need to be an expert. If you can write an add_action(), you can follow along.

What a WordPress Nonce Actually Is

The word "nonce" means "number used once" in cryptography. The WordPress version is not that. The WordPress developer handbook says it plainly: despite the name it is a hash of numbers and letters, and it is not checked for one-time use, so it does not prevent replay attacks. You can read that on the official WordPress nonces documentation.

What you get is a short-lived token generated from three things:

  • The user (and their session), so one person's nonce does not work for another.
  • The action, a string you choose, such as save_my_settings, so a nonce for one job cannot be used for another.
  • The time, so the token goes stale after a while.

When the form is submitted, WordPress calculates what the token should be for this user, action and time, and compares it with what arrived. A match means the request very likely came from a page your site produced for this person.

Think of it as a ticket stub with your name, the event and today's date on it. It proves you were handed it by the venue. It does not prove you are allowed backstage.

What a Nonce Protects Against (and What It Does Not)

It Protects Against CSRF

Cross-site request forgery is when an attacker tricks a signed-in user into sending a request they never meant to. Imagine an administrator is signed in to your site, then opens a malicious page in another tab. That page quietly submits a form to yoursite.com/wp-admin/ such as "delete this user". The browser attaches the admin's cookies, so without any extra check, the site obeys.

A nonce breaks this. The attacker's page cannot know the nonce for that admin and that action, so the forged request fails verification.

It Does Not Protect Authorization

This is the part people get wrong. The official documentation states that nonces should never be relied on for authentication, authorization or access control, and tells you to use current_user_can() to protect functions.

A nonce answers "did this request come from my page?" It does not answer "is this person allowed to do this?" A subscriber can load a page, get a perfectly valid nonce for an action and submit it. If your handler only checks the nonce, you have just let a subscriber do an admin job.

Rule of thumb: nonce check plus capability check, every time. No exceptions.

It also does not clean your data. You still sanitize input, escape output and use $wpdb->prepare() for queries. (If you want a quick tour of how these mistakes show up in the wild, see our article on WordPress plugin vulnerabilities.)

Creating and Checking a Nonce in PHP

WordPress gives you a small set of functions. Here is what each does.

FunctionUse it to
wp_create_nonce( $action )Return a nonce string for an action
wp_nonce_field( $action, $name )Print hidden form fields containing a nonce
wp_nonce_url( $url, $action )Add a nonce to a link
check_admin_referer( $action, $name )Verify a nonce (and referrer) in an admin request, die on failure
check_ajax_referer( $action, $name )Verify a nonce in an AJAX request
wp_verify_nonce( $nonce, $action )Verify a nonce yourself and handle the result

A Form With a Nonce

Here is a complete settings form handler. Notice the three layers: nonce, capability, then sanitizing.

<?php
// Rendering the form.
function myplugin_render_form() {
    ?>
    <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
        <input type="hidden" name="action" value="myplugin_save">
        <?php wp_nonce_field( 'myplugin_save_settings', 'myplugin_nonce' ); ?>
        <input type="text" name="myplugin_title" value="">
        <?php submit_button( 'Save' ); ?>
    </form>
    <?php
}

// Handling the submit.
add_action( 'admin_post_myplugin_save', 'myplugin_handle_save' );
function myplugin_handle_save() {
    check_admin_referer( 'myplugin_save_settings', 'myplugin_nonce' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( esc_html__( 'You are not allowed to do that.', 'myplugin' ), 403 );
    }

    $title = isset( $_POST['myplugin_title'] )
        ? sanitize_text_field( wp_unslash( $_POST['myplugin_title'] ) )
        : '';

    update_option( 'myplugin_title', $title );

    wp_safe_redirect( admin_url( 'admin.php?page=myplugin&updated=1' ) );
    exit;
}

wp_nonce_field() prints two hidden inputs: your nonce under the name you chose, and a _wp_http_referer field. check_admin_referer() reads them back. If the check fails, WordPress stops with the familiar "The link you followed has expired" screen.

A Link With a Nonce

For actions triggered by a link, such as "delete this item", use wp_nonce_url():

$delete_url = wp_nonce_url(
    admin_url( 'admin.php?page=myplugin&do=delete&item=42' ),
    'myplugin_delete_42'
);

Then in the handler:

check_admin_referer( 'myplugin_delete_42' );

Notice the action name includes the item ID. That makes the nonce specific to this one thing. It is a small touch that makes it harder to reuse a nonce from one item on another.

Checking by Hand

When you need to decide what happens on failure instead of dying, use wp_verify_nonce():

$nonce = isset( $_POST['myplugin_nonce'] )
    ? sanitize_text_field( wp_unslash( $_POST['myplugin_nonce'] ) )
    : '';

if ( ! wp_verify_nonce( $nonce, 'myplugin_save_settings' ) ) {
    wp_send_json_error( array( 'message' => 'Security check failed.' ), 403 );
}

It returns 1 if the nonce is from the first half of its life, 2 if from the second half and false if invalid. Treat any truthy value as a pass.

Using a WordPress Nonce With AJAX and JavaScript

For admin-ajax requests, create the nonce in PHP and hand it to your script with wp_localize_script() or wp_add_inline_script().

add_action( 'admin_enqueue_scripts', function () {
    wp_enqueue_script( 'myplugin-admin', plugins_url( 'admin.js', __FILE__ ), array(), '1.0.0', true );
    wp_localize_script( 'myplugin-admin', 'myplugin', array(
        'ajaxUrl' => admin_url( 'admin-ajax.php' ),
        'nonce'   => wp_create_nonce( 'myplugin_ajax' ),
    ) );
} );

add_action( 'wp_ajax_myplugin_do_thing', function () {
    check_ajax_referer( 'myplugin_ajax', 'nonce' );

    if ( ! current_user_can( 'edit_posts' ) ) {
        wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 );
    }

    wp_send_json_success( array( 'ok' => true ) );
} );

And the JavaScript:

const body = new URLSearchParams( {
  action: 'myplugin_do_thing',
  nonce: myplugin.nonce,
} );

fetch( myplugin.ajaxUrl, { method: 'POST', body } )
  .then( ( response ) => response.json() )
  .then( ( result ) => console.log( result ) );

check_ajax_referer() dies by default when the check fails. Pass false as the third argument if you would rather get a return value and send your own JSON error.

The REST API and the wp_rest Nonce

(For the bigger picture beyond nonces, see WordPress REST API security.) If you use the REST API from JavaScript on a page where the user is signed in, WordPress authenticates them by cookie. To guard against CSRF, it also requires a nonce created with the action wp_rest.

According to the REST API authentication handbook, you can send it in the X-WP-Nonce header or as a _wpnonce parameter. The header is the more reliable choice, especially for DELETE requests. And without a valid nonce, the API sets the current user to 0, so the request is treated as logged out even though you are signed in.

wp_localize_script( 'myplugin-app', 'mypluginRest', array(
    'root'  => esc_url_raw( rest_url() ),
    'nonce' => wp_create_nonce( 'wp_rest' ),
) );
fetch( mypluginRest.root + 'myplugin/v1/items', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': mypluginRest.nonce,
  },
  body: JSON.stringify( { title: 'Hello' } ),
} );

On the server side, you still set a permission_callback on your route that checks capabilities. The wp_rest nonce only proves the cookie request is intended. It is not a permission check.

register_rest_route( 'myplugin/v1', '/items', array(
    'methods'             => 'POST',
    'callback'            => 'myplugin_create_item',
    'permission_callback' => function () {
        return current_user_can( 'edit_posts' );
    },
) );

How Long Does a WordPress Nonce Last?

By default, a nonce is valid for one day, and you can change that with the nonce_life filter, which takes seconds. The handbook adds the detail most people miss: WordPress uses two 12-hour ticks and accepts a nonce from the current tick or the previous one. With the default setting, a nonce's real lifetime is therefore between 12 and 24 hours.

add_filter( 'nonce_life', function () {
    return 4 * HOUR_IN_SECONDS;
} );

Be careful with this filter. It applies to every nonce on the site, including those made by other plugins, so shortening it can make other people's forms fail more often. A longer life gives attackers more time. For most sites, the default is the sensible choice.

The practical consequence for users: if someone leaves an edit screen open overnight and then clicks Save, the nonce may have expired. WordPress's admin handles some of this by refreshing nonces through the Heartbeat API, but your own custom pages may not.

Caching Gotchas

This is the problem that causes the most head-scratching in real projects.

A nonce belongs to a specific user (and for logged-out visitors, to a shared "no user" identity) and a limited time window. A full-page cache stores a page's HTML and serves the same copy to many people. If that copy contains a nonce, everyone after the first visitor receives a token that was made for someone else, or that has since expired.

Typical symptoms:

  • A front-end form that works for you but fails for visitors.
  • "The link you followed has expired" or a plain 403 on submit.
  • A form that works for a few hours after the cache is cleared, then breaks.
  • AJAX calls that fail only for logged-out users.

How to handle it:

  1. Do not put nonces in cacheable HTML for public forms. Fetch a fresh nonce with a small request after the page loads, then add it to the form with JavaScript.
  2. Exclude the pages from the cache if they must contain a nonce in the markup.
  3. Do not use a nonce as your only protection on public forms. For logged-out visitors, a nonce is a weak signal because there is no user to tie it to. Use rate limiting or CAPTCHA for spam and abuse.
  4. Shorten cache lifetimes below the nonce lifetime if a cached page must carry a nonce. Anything past the nonce lifetime is guaranteed stale.

Also remember a nonce does not protect public, logged-out actions the way it protects signed-in ones. It was designed around a logged-in user.

Common Mistakes With Nonces

  • Using the nonce as a permission check. Always add current_user_can().
  • Using the same action string everywhere. Use specific names, ideally with an object ID, such as myplugin_delete_42.
  • Forgetting to verify on the handler. A form with wp_nonce_field() and no check in the handler is decoration.
  • Verifying only when the field is present. if ( isset( $_POST['n'] ) && wp_verify_nonce( ... ) ) quietly skips the check when someone omits the field. Fail when it is missing.
  • Using a nonce on a GET request that changes data. Better to use POST for changes. If you must use a link, use wp_nonce_url() and verify it.
  • Printing a nonce into a cached public page. See the section above.
  • Not unslashing and sanitizing the nonce value before passing it to wp_verify_nonce().
  • Putting nonces in URLs that get shared. Nonces in a link may end up in logs, history and referrer headers. Keep their life short and do not treat them as secrets.
  • Assuming a nonce stops replay. It is not one-time. Within its lifetime, the same nonce works more than once.

A Quick Word on Stack Guard for Developers

If you build or maintain sites that run Stack Guard, the part that will interest you is the Stack Guard developer reference. It lists the plugin's public actions and filters, such as hooks that fire on sign-in lockouts, bans, scan results and settings changes, along with the wp-config.php constants and WP-CLI commands it supports. That lets you connect your own logging, alerts or tooling to what the plugin does. There is a page for developers that explains how it fits in a workflow too.

On the hardening side, Hardening can switch off the built-in theme and plugin code editors, so a stolen admin session cannot be used to write code into your site (see the admin protection documentation). That is a good companion to nonces and capability checks: they stop forged and unauthorized requests, and this limits the damage if an account is taken over anyway. Short sessions help in the same way, and our guide to WordPress auto logout shows how to set them up.

A Checklist You Can Copy

For any handler that changes something:

  1. Output the nonce with wp_nonce_field(), wp_nonce_url() or wp_create_nonce().
  2. Verify it with check_admin_referer(), check_ajax_referer() or wp_verify_nonce(), and fail if it is missing.
  3. Check the user's capability with current_user_can().
  4. Sanitize every input and use $wpdb->prepare() for queries.
  5. Escape every output.
  6. For REST routes, use a permission_callback and send the wp_rest nonce in X-WP-Nonce.
  7. Keep nonces out of cached public pages.

Do those seven things consistently and you will have covered most of the ways a WordPress nonce gets misused.

Stack Guard

Explore the Stack Guard Hook Reference

Stack Guard publishes a reference of its actions, filters, constants and WP-CLI commands for developers.

Open the Developer Reference
Share this article
  • #nonce
  • #csrf
  • #development

Questions about WordPress nonce

What is a WordPress nonce?

A WordPress nonce is a token tied to a user, an action and a time window that helps confirm a request really came from your own form or link. It is meant to stop cross-site request forgery. Despite the name, it is not a true number used once.

How long does a WordPress nonce last?

By default a nonce is valid for one day, which WordPress handles as two 12-hour ticks, so the real lifetime is between 12 and 24 hours. You can change it with the nonce_life filter, which takes a value in seconds.

Is a WordPress nonce enough to secure a form?

No. A nonce checks that the request came from your page, not that the user is allowed to do the thing. Always pair it with a capability check such as current_user_can(), and sanitize and escape your data.

What is the difference between wp_verify_nonce and check_admin_referer?

wp_verify_nonce() only checks a nonce and returns a result for you to handle. check_admin_referer() checks the nonce and the referrer, then stops the request with an error screen if it fails.

How do I use a WordPress nonce with the REST API?

Create a nonce for the wp_rest action, pass it to your JavaScript and send it in the X-WP-Nonce header. Without it, the REST API treats a cookie-based request as coming from a logged-out user.

Why do I get the error 'The link you followed has expired'?

It usually means a WordPress nonce failed verification: the page sat open too long, the nonce was missing or the action name did not match. A cached page serving an old nonce is another common cause.

Can caching break a WordPress nonce?

Yes. If a full-page cache stores a page containing a nonce, later visitors get a stale token that fails or belongs to someone else. Keep nonces out of cached pages or fetch them with JavaScript after the page loads.

Get new articles by email

One short email when a new article is out, plus the WordPress threats worth knowing about. No spam, unsubscribe any time.

Lock down your WordPress site today

Free forever on every site. Pro when you want automation and alerts.