Skip to main content
WP HealthKit

WordPress Nonces Explained: A CSRF Protection Guide

March 30, 202615 min readSecurityBy Jamie

WordPress nonces are one of the most misunderstood security features in the platform. Developers often treat them as magic tokens to paste into forms without understanding what they actually do—or why they matter. The truth is simpler: nonces protect your WordPress site from one specific, devastating type of attack called Cross-Site Request Forgery (CSRF). This guide explains what nonces are, why WordPress needs them, how they compare to alternatives like JWT tokens, and critically, when you should not use them. If you're building plugins, themes, or custom WordPress code, understanding nonces deeply will make your code more secure and your debugging faster.

Table of Contents

  1. What Is CSRF and Why Should You Care
  2. How WordPress Nonces Work
  3. Nonces vs. JWT: When to Use Each
  4. Implementing Nonces Correctly
  5. Common Nonce Mistakes and How to Avoid Them
  6. When NOT to Use Nonces
  7. Security Auditing for Nonce Issues
  8. Frequently Asked Questions

What Is CSRF and Why Should You Care

Cross-Site Request Forgery is a real, exploitable vulnerability that has impacted millions of websites. Let's walk through a concrete scenario.

Imagine you're logged into your WordPress admin dashboard. You're reading email in another tab. A malicious link in that email points to a site controlled by an attacker. That site contains hidden code that makes a request to your WordPress site—something like "delete all posts" or "create a new admin user."

Your browser automatically includes your WordPress session cookies with that request. From WordPress's perspective, the request came from you. It came from your browser. It was authenticated. WordPress processes it.

This is CSRF: an attacker tricks your browser into making a request to a site where you're already authenticated, and your browser helplessly sends along your cookies. The attacker never needs your password. They never need to log in themselves.

The vulnerability is particularly dangerous in WordPress because:

Admin actions have broad power. A single CSRF attack could delete all posts, modify user roles, activate malicious plugins, or inject code into your theme. A compromised admin account is a complete site takeover.

Cookies are sent automatically. HTML forms, image tags, and JavaScript requests all include cookies. The user doesn't know anything is happening. They just visited a page.

Default browser behavior enables it. Browsers were designed to include cookies in cross-origin requests. This is a feature, not a bug—it allows websites to display your profile picture from another site. But it opens the door to CSRF.

OWASP lists CSRF in its top 10 vulnerabilities for good reason. It's simple to exploit, devastating in impact, and easy to prevent if you know how.

How WordPress Nonces Work

A nonce (number used once) is WordPress's answer to CSRF. It's a short-lived, one-time token that proves a request came from your site and not from an attacker.

The Core Idea

When you load a WordPress form (a plugin settings page, a comment form, anything that modifies data), WordPress generates a unique token and includes it in the form as a hidden field. When the form submits, that token travels with the request. WordPress verifies the token before processing the request. If the token is missing or invalid, the request is rejected.

An attacker can trick your browser into making a request, but they can't access the nonce token. Nonces are not stored in cookies. They're embedded in the page HTML. CSRF attacks rely on cookies being sent automatically; they have no way to steal a nonce from another page.

How Nonces Are Generated

WordPress generates nonces using the wp_create_nonce() function. Behind the scenes, it does this:

$nonce_token = wp_create_nonce( 'my-custom-action' );

WordPress creates a hash using three inputs:

  1. A static secret key (the NONCE_SALT and NONCE_KEY constants, or a hash of AUTH_KEY if not defined)
  2. An action identifier (the string you pass, like 'my-custom-action')
  3. A timestamp (divided into 12-hour blocks)

The result is a unique, time-limited token. Here's the crucial part: because nonces use a timestamp divided into blocks, the same nonce is valid for 12-24 hours (the current block and potentially the previous one, depending on timing). After that, it expires.

How Nonce Verification Works

When the form submits, WordPress checks the nonce using wp_verify_nonce():

if ( ! isset( $_POST['my_nonce_field'] ) || ! wp_verify_nonce( $_POST['my_nonce_field'], 'my-custom-action' ) ) {
    wp_die( 'Nonce verification failed' );
}

WordPress regenerates the hash using the same inputs (the secret key, the action, and the current timestamp block). If it matches the token sent with the request, the nonce is valid.

Why This Prevents CSRF

The attacker cannot regenerate the nonce. They don't know the NONCE_SALT. They can't access the nonce from the original page. They can't predict what it will be. Even if they manage to get your browser to make a request, the request won't include the correct nonce, and WordPress will reject it.

The nonce is time-limited, so even if an attacker somehow steals a nonce, it becomes useless after 24 hours. And the nonce is tied to a specific action, so a nonce generated for "delete-post" won't work for "create-user."

Nonces vs. JWT: When to Use Each

JWT (JSON Web Token) and WordPress nonces are both security tokens. They solve different problems, and that difference matters.

What JWT Does

JWT is a stateless authentication token commonly used in REST APIs and modern web applications. It contains encoded claims (data) that the server trusts because they're cryptographically signed. JWTs can include user identity, permissions, and expiration time.

// Example JWT structure (simplified)
$jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." .
       "eyJzdWIiOiJ1c2VyLTEyMzQ1IiwibmFtZSI6IkpvaG4gRG9lIn0." .
       "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";

A JWT proves who you are and what you're allowed to do. It's ideal for REST APIs where stateless authentication is preferred.

What Nonces Do

Nonces prove that a request came from your site, not from elsewhere. They don't contain user information. They don't replace authentication. They work alongside cookies.

The Critical Difference

A nonce stops CSRF attacks. A JWT does not.

If an attacker tricks your browser into making a request to your REST API, and you authenticate that request with a JWT (perhaps stored in localStorage and sent in an Authorization header), the API will process it. The JWT proves the request is from an authenticated user, but it doesn't prove the request originated from your own website. Your REST API endpoint is still vulnerable to CSRF.

When to Use Nonces

Use nonces for form submissions in WordPress admin pages, deleting or modifying content in your plugin, any action triggered by a form POST request, and custom AJAX actions in WordPress admin.

Nonces are built into WordPress and require zero additional setup. They integrate seamlessly with wp_nonce_field() for forms and wp_nonce_url() for action links.

When to Use JWT

Use JWT for REST API authentication where stateless verification is needed, third-party integrations that can't rely on WordPress session cookies, mobile app backends, and situations where you need claims beyond simple verification (user roles, permissions).

Can You Use Both?

Yes, and often you should. A form submission can include both a JWT and a nonce. A REST API endpoint can verify the JWT and check for a nonce in the POST body. If you're building a REST API in WordPress, include nonce verification in addition to JWT or other authentication.

Implementing Nonces Correctly

Let's move from theory to practice. Here's how to properly implement nonces in your WordPress code.

Protecting a Form Submission

Start with a simple plugin settings form:

// On the settings page, output the form with a nonce field
echo '<form method="post">';
wp_nonce_field( 'save_my_settings', 'my_settings_nonce' );
echo '<input type="text" name="setting_value" />';
echo '<button type="submit">Save Settings</button>';
echo '</form>';

The wp_nonce_field() function outputs:

<input type="hidden" id="my_settings_nonce" name="my_settings_nonce" value="a1b2c3d4e5f6" />

Then, handle the form submission:

if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
    // Verify the nonce
    if ( ! isset( $_POST['my_settings_nonce'] ) ||
         ! wp_verify_nonce( $_POST['my_settings_nonce'], 'save_my_settings' ) ) {
        wp_die( 'Security check failed. Please try again.' );
    }

    // Process the form
    update_option( 'my_plugin_setting', sanitize_text_field( $_POST['setting_value'] ) );
    echo 'Settings saved.';
}

The two action strings must match exactly. If they don't, verification fails.

Protecting AJAX Requests

For AJAX actions, pass the nonce in the request:

PHP (enqueue script with nonce):

wp_enqueue_script( 'my-plugin-script', plugin_dir_url( __FILE__ ) . 'js/script.js' );
wp_localize_script( 'my-plugin-script', 'myPluginData', array(
    'nonce' => wp_create_nonce( 'my_ajax_action' ),
) );

JavaScript:

const data = new FormData();
data.append( 'action', 'my_ajax_action' );
data.append( 'nonce', myPluginData.nonce );
data.append( 'user_input', 'some value' );

fetch( '/wp-admin/admin-ajax.php', {
    method: 'POST',
    body: data,
} )
.then( response => response.json() )
.then( data => console.log( data ) );

PHP (handle the AJAX request):

add_action( 'wp_ajax_my_ajax_action', 'my_ajax_handler' );

function my_ajax_handler() {
    if ( ! isset( $_POST['nonce'] ) ||
         ! wp_verify_nonce( $_POST['nonce'], 'my_ajax_action' ) ) {
        wp_send_json_error( 'Nonce verification failed', 403 );
    }

    // Process the AJAX request
    $result = do_something( sanitize_text_field( $_POST['user_input'] ) );
    wp_send_json_success( $result );
}

For simple links that trigger an action (like "Delete" or "Approve"), use wp_nonce_url():

$delete_url = wp_nonce_url(
    admin_url( 'admin.php?page=my-plugin&action=delete&id=' . $item_id ),
    'delete_item_' . $item_id,
    'nonce'
);

echo '<a href="' . esc_url( $delete_url ) . '">Delete</a>';

Then verify:

if ( isset( $_GET['action'] ) && $_GET['action'] === 'delete' ) {
    if ( ! isset( $_GET['nonce'] ) ||
         ! wp_verify_nonce( $_GET['nonce'], 'delete_item_' . $_GET['id'] ) ) {
        wp_die( 'Nonce verification failed' );
    }

    // Delete the item
}

Quick Audit

Wondering if your plugin handles nonces correctly? WP HealthKit checks for all of these patterns and 40+ more across 17 verification layers — including Wordfence CVE cross-referencing, PHPCS coding standards, and PHPStan type safety analysis.

Run a free audit →


Common Nonce Mistakes and How to Avoid Them

Even experienced developers slip up with nonces. Here are the most common mistakes and how to fix them.

Mistake 1: Using the Same Nonce Action for Everything

// Bad: Same action string for all nonces in a plugin
wp_nonce_field( 'my_plugin_nonce', 'nonce' );

If you use the same action string everywhere, a nonce meant for deleting a post could theoretically be reused for other actions.

Fix: Use unique action strings that describe the intent:

wp_nonce_field( 'delete_post_' . $post_id, 'nonce' );
wp_nonce_field( 'update_settings', 'nonce' );

Mistake 2: Not Sanitizing POST Data After Nonce Check

A nonce verifies the request came from your site, but it doesn't validate the data inside the request.

// Bad: Nonce is checked, but data isn't validated
if ( wp_verify_nonce( $_POST['nonce'], 'update_settings' ) ) {
    $user_input = $_POST['user_input'];
    update_option( 'my_setting', $user_input );
}

Always sanitize and validate user input:

if ( wp_verify_nonce( $_POST['nonce'], 'update_settings' ) ) {
    $user_input = sanitize_text_field( $_POST['user_input'] );
    update_option( 'my_setting', $user_input );
}

Mistake 3: Checking Nonce in the Wrong Place

If you check the nonce too late (after you've already started processing), you might have already modified data:

// Bad: Data processed before nonce check
if ( isset( $_POST['user_input'] ) ) {
    $data = process_user_input( $_POST['user_input'] );
}

if ( ! wp_verify_nonce( $_POST['nonce'], 'action' ) ) {
    wp_die( 'Nonce failed' );
}

Fix: Check the nonce first, before any data processing:

if ( isset( $_POST['nonce'] ) &&
     wp_verify_nonce( $_POST['nonce'], 'action' ) ) {
    $data = process_user_input( sanitize_text_field( $_POST['user_input'] ) );
} else {
    wp_die( 'Nonce failed' );
}

Mistake 4: Mismatched Action Strings

// Bad: Action strings don't match
wp_nonce_field( 'save-settings', 'nonce' );

// Later, in the handler:
wp_verify_nonce( $_POST['nonce'], 'save_settings' );  // Hyphens vs underscores

Fix: Use a constant to ensure consistency:

define( 'MY_PLUGIN_ACTION', 'save_settings' );

wp_nonce_field( MY_PLUGIN_ACTION, 'nonce' );

if ( wp_verify_nonce( $_POST['nonce'], MY_PLUGIN_ACTION ) ) {
    // Process
}

When NOT to Use Nonces

Nonces are powerful for form submissions, but they're not the right tool for every scenario. Understanding when not to use them is just as important as knowing when to use them.

Don't Use Nonces for Authentication

Nonces are not passwords. They don't prove who you are. They prove a request came from your site.

// Bad: Using a nonce as authentication
if ( wp_verify_nonce( $_GET['nonce'], 'user_action' ) ) {
    echo 'User logged in!';  // Wrong
}

// Good: Check user is logged in, then verify nonce
if ( ! is_user_logged_in() ) {
    wp_die( 'Please log in first' );
}

if ( ! wp_verify_nonce( $_GET['nonce'], 'user_action' ) ) {
    wp_die( 'Nonce verification failed' );
}

Don't Use Nonces for Public-Facing Forms

Nonces are designed for WordPress admin pages and authenticated actions. Public forms need additional validation: CAPTCHAs, rate limiting, IP-based filtering, or email verification. A malicious bot can visit your public page, read the nonce, and submit the form with a valid nonce.

Don't Use Nonces for API Keys or Long-Lived Tokens

Nonces expire after 24 hours. If you're building a system where external services need to authenticate to your WordPress site, use persistent API keys, OAuth tokens, or JWT for API authentication.

Don't Use Nonces Without HTTPS

Nonces are passed in the request body or URL. If your site doesn't use HTTPS, an attacker on the same network could intercept the nonce and use it to craft a valid request. Always use HTTPS in production.

Don't Rely on Nonces Alone for Admin Protection

Nonces protect against CSRF, but they don't protect against account takeover, weak passwords, or plugin vulnerabilities. A comprehensive security strategy includes strong authentication, regular security audits, keeping WordPress updated, and using security plugins.

Security Auditing for Nonce Issues

If you're auditing WordPress code for nonce vulnerabilities, here's what to look for.

Signs of Missing Nonce Protection

Search your codebase for these patterns:

  1. Form submissions without nonces: Any $_SERVER['REQUEST_METHOD'] === 'POST' handler without wp_verify_nonce()
  2. Admin actions without nonces: Any $_GET['action'] handler without nonce verification
  3. AJAX handlers without nonces: Any wp_ajax_ hook without check_ajax_referer() or wp_verify_nonce()

Automated Detection

Tools like WP HealthKit scan your WordPress site for these exact issues. Instead of manually reviewing thousands of lines of code, automated security audits identify missing or improperly implemented nonces across all your plugins and custom code.

For a deeper look at all the security patterns WP HealthKit checks for, see our complete guide: Top 10 Security Mistakes in WordPress Plugins. You can also review the WordPress Plugin Security Handbook and OWASP CSRF Prevention Cheat Sheet for additional context.

Frequently Asked Questions

Can an attacker steal a nonce and reuse it?

Not easily. Nonces are embedded in page HTML, not stored in cookies. An attacker would need to somehow access the page where the nonce is displayed, extract it, and use it within 24 hours. In practice, nonce theft is far less common than CSRF attacks without nonces at all.

Why does WordPress divide time into 12-hour blocks instead of using absolute timestamps?

The 12-hour blocks allow nonces generated near the boundary between time blocks to remain valid through both blocks. If WordPress used exact Unix timestamps, a nonce generated at 11:59 PM would expire at 11:59 PM the next day, even though to the user it feels like it was just created. The 12-hour block system is more forgiving while still providing time-based expiration.

Do I need nonces for read-only requests (GET requests)?

No. CSRF attacks are dangerous because they can modify data. If your request only reads data and doesn't make changes, a CSRF attack is less harmful. However, it's still good practice to protect any admin page with nonces.

What if a user's session expires while they're filling out a form?

The nonce will still be valid (as long as it's within the 24-hour window), but if their WordPress session has expired, they'll be logged out. When they submit the form, WordPress will redirect them to the login page.

Can I create a nonce that lasts longer than 24 hours?

Not with WordPress's default nonce system. Nonces are intentionally short-lived. If you need a longer-lived token, use a custom system with persistent tokens stored in the database. Many OAuth and JWT systems provide 30-day or longer tokens for this reason.


Conclusion

WordPress nonces are a simple but effective defense against CSRF attacks. They work by embedding a time-limited, cryptographically verified token in your forms and checking that token when the form is submitted. An attacker can't create a valid nonce because they don't know the secret key. They can't steal your nonce because it's not stored in a cookie.

But nonces are not a silver bullet. They don't protect against weak passwords, they don't authenticate users, and they don't validate data. They're one layer in a comprehensive security strategy.

The most common nonce mistakes are forgetting to include them, mismatching action strings, checking them too late, or using them for the wrong purpose entirely. A good security audit catches these issues before an attacker does.

If you're building WordPress plugins or custom WordPress code, make nonces a habit. Combine nonce verification with WP HealthKit's automated scanning across our 17 verification layers to catch issues your team might miss. Every form that modifies data should include a nonce. Every AJAX action should verify one. Every admin link that triggers an action should include one.


Secure Your Plugin Today

Start with a free security audit of your WordPress plugin to see how your current code handles nonces and other CSRF protection measures.

Run a free audit → — No credit card required.

Ready to audit your plugin?

WP HealthKit checks for all the issues in this article and 40+ more across 49 verification layers.

Comments

WordPress Nonces Explained: A CSRF Protection Guide | WP HealthKit