☰
WORDPRESS / PRACTICE

WordPress Nonces on Cached Pages: Why a 403 Error Occurs and How to Fix It

WordPress Nonces on Cached Pages: Why a 403 Error Occurs and How to Fix It

After page caching is enabled in WordPress, a site may start returning the error `403 - Are you sure you want to do this?`. Often, the problem is not AJAX, a form, or the REST API itself, but a nonce that was inserted into the page HTML and then stored in the cache.

A WordPress nonce is a short-lived security token used to verify the intent behind a request. If the cache serves an outdated nonce or one created in a different context, the server rejects the request. Below, we will explain how to confirm the issue, which fixes are safe, and which mistakes to avoid.

What Is a WordPress Nonce and What Is It Used For?

A WordPress nonce is not a password or a complete authentication mechanism. It helps confirm that a request came from a form or interface that WordPress considers valid, rather than being forged directly by an external site.

Nonces are usually created with `wp_create_nonce()` and verified with `wp_verify_nonce()`, `check_ajax_referer()`, or the built-in REST API verification process. A token is associated with the user's session and a time interval. In WordPress, a nonce is generally valid for a limited period that may span the current or previous time interval, so it should not be treated as a permanent key.

If an operation changes data, the nonce should be only one layer of protection. The server must also verify user permissions with `current_user_can()` or another appropriate mechanism.

  • For AJAX requests, a nonce is often passed in a POST parameter or a separate HTTP header.
  • For the REST API, WordPress typically uses the `X-WP-Nonce` header.
  • A nonce does not replace session, role, capability, or input sanitization checks.

How Page Caching Breaks Nonces

The problem occurs when HTML containing a nonce is stored in a page cache, CDN cache, reverse proxy, or optimization plugin cache. The next visitor receives a previously stored copy of the page instead of fresh HTML generated for their request.

This is especially critical for authenticated users: the page may contain a nonce created for a different session. Even if the page looks identical for everyone, the token inside it may be personalized.

An outdated nonce can also appear when the cache lasts longer than the token's validity period. In that case, the error may not appear immediately after the page is published but only after some time has passed.

A typical symptom is that everything works for an administrator or immediately after the cache is cleared, while a form or AJAX request fails for a regular user. Reloading the page may temporarily change the result, making the issue more difficult to diagnose.

  • The page HTML is cached together with a hidden nonce field.
  • The CDN serves an old copy of the document containing an expired token.
  • The cache may combine public HTML with personalized data.
  • Clearing only the browser cache will not help if the old copy remains on the server or CDN.

How to Confirm That Caching Is the Cause

Start by comparing the page behavior with page caching enabled and disabled. If bypassing the page cache temporarily makes the request work, this is a strong indication of a conflict between the cache and the nonce.

Inspect the page HTML and find the field or variable that contains the nonce. Then open the browser DevTools, go to the Network tab, and check which value is actually sent in the AJAX or REST request.

Pay attention to the response headers for the HTML page. They may show whether the page was served from a cache, for example through service headers added by a CDN or caching plugin. The exact header names depend on the infrastructure, so do not rely on a single header alone.

Test several scenarios: an authenticated user, a guest, a private browsing window, different devices, and reopening the page after the cache lifetime has expired. It is also important to check not only the frontend but the server-side handler itself.

  • Inspect the Network tab and check the status of the AJAX or REST request.
  • Verify that `X-WP-Nonce` is being sent specifically with the REST request.
  • Temporarily clear the page cache, object cache, and CDN cache if one is being used.
  • Review server logs or the WordPress log to distinguish a nonce error from a permissions error.
  • Make sure the server does not accept a request merely because a nonce parameter is present without actually verifying it.

The Most Reliable Ways to Fix the Problem

The right solution depends on whether the page is personalized and how important it is to cache the entire page. You do not need to disable the site's entire cache because of one form. In most cases, it is enough to separate public content from dynamic operations.

  • Do not cache pages containing personalized forms, user accounts, carts, checkout pages, or other session-based data. Exclude these pages from page caching at the plugin, server, and CDN levels.
  • Retrieve the nonce through a separate uncached AJAX or REST request after the page loads. This keeps the main HTML fast and public while requesting the token immediately before the operation.
  • For an endpoint that returns a nonce or other personalized data, use appropriate headers such as `Cache-Control: no-store`. This must be configured on the server or in the endpoint code, not only in the browser.
  • Separate caching by cookie or authentication state. Authenticated users should not receive the same cached HTML copy as anonymous visitors.
  • For REST requests, send a fresh token in `X-WP-Nonce` instead of storing it in long-lived cached HTML.

Example: Retrieve a Nonce with a Separate Request

The following pattern shows a general approach: the page can be cached, while the nonce is retrieved after the page loads. The actual URL and response format depend on your endpoint.

fetch('/wp-json/my-plugin/v1/nonce', {
  method: 'GET',
  credentials: 'same-origin',
  cache: 'no-store'
})
  .then(response => response.json())
  .then(data => {
    const nonce = data.nonce;
    // Використайте nonce у наступному запиті.
  });

Pass the Nonce in a REST Request

For a standard WordPress REST API integration, pass the nonce in the `X-WP-Nonce` header. The server must still verify authentication, permissions, and input data.

fetch('/wp-json/my-plugin/v1/item', {
  method: 'POST',
  credentials: 'same-origin',
  headers: {
    'Content-Type': 'application/json',
    'X-WP-Nonce': nonce
  },
  body: JSON.stringify({ title: 'Приклад' })
});

Review the Server-Side Code: A Nonce Does Not Replace Authorization

Even a fresh nonce does not mean that the user is authorized to perform an operation. The handler must separately verify the appropriate capability, such as `edit_posts`, `manage_options`, or a custom permission that matches the specific action.

For AJAX requests, verify the nonce at the beginning of the handler using the appropriate WordPress mechanism. For the REST API, return an error if the user is not authenticated or lacks the required permissions. Then sanitize and validate all input values.

Do not try to solve the caching problem by extending the nonce lifetime or disabling nonce verification. This may hide the symptom, but it weakens security and does not resolve the personalization issue in the HTML.

  • A nonce verifies the freshness and origin of a request, but it does not prove that the user has the required role.
  • Operations that change, delete, or publish data must verify the user's capability.
  • Access errors should be logged without recording the tokens themselves in plain text.
  • Do not pass a nonce through the URL unless there is a clear need: URLs may appear in logs, browser history, and third-party systems.
if ( ! check_ajax_referer( 'my_action', 'nonce', false ) ) {
    wp_send_json_error( 'Недійсний nonce', 403 );
}

if ( ! current_user_can( 'edit_posts' ) ) {
    wp_send_json_error( 'Недостатньо прав', 403 );
}

// Далі: валідація даних і виконання операції.

Common Mistakes When Configuring Caching

The most common mistake is caching the entire page without analyzing its contents. A public article or page may be safe to cache, but a form containing a nonce makes the HTML partially dynamic.

Another problem is clearing only one cache layer. An old copy may remain in a plugin, web server, CDN, or browser. After changing the rules, check every layer and wait for it to update or clear the relevant cache manually.

Do not place a nonce in a JavaScript file with a long cache lifetime. If a script contains a token, it effectively becomes a personalized resource. It is better to pass fresh values through an uncached endpoint or localized data while taking the cache rules into account.

It is also dangerous to serve a page containing personal data under a shared public URL. In this case, the problem is broader than the nonce: the cache may show one user's data to another user.

  • Do not rely only on clearing the browser cache.
  • Do not cache responses from an endpoint that returns a nonce or session data.
  • Do not disable nonce verification because of 403 errors.
  • Do not use one static token for all users.
  • After making changes, test the page both as a guest and as an authenticated user.

Practical Pre-Launch Checklist

Before enabling full-page caching, test every scenario in which the HTML contains a nonce or other session data. This helps identify the problem before users encounter it.

  • Create a list of pages containing forms, AJAX actions, REST operations, and user account areas.
  • Determine which pages can be cached as public content and which must be excluded.
  • Check whether nonces are appearing in cached HTML, JavaScript, or CDN responses.
  • For dynamic tokens, provide a separate endpoint with `Cache-Control: no-store`.
  • Check the `X-WP-Nonce` header in REST requests.
  • Make sure the server verifies capabilities, sessions, nonces, and input data.
  • Test behavior after the cache lifetime expires and after all cache layers have been force-cleared.

Frequently asked questions

Do I need to disable WordPress caching completely if a nonce error occurs?

No. Usually, you only need to exclude personalized pages and endpoints that return nonces or session data from the page cache. Public pages without dynamic tokens can continue to be cached.

Why does the nonce work for an administrator but not for a regular user?

An administrator may receive a fresh page without caching or have a different cookie context. A regular user often receives a shared cached HTML copy containing someone else's nonce or an expired token.

Can I simply increase the nonce lifetime?

This is not the correct solution. Increasing the lifetime may reduce the level of protection, but it will not fix a nonce that has already been stored in cached HTML. You need to change the caching strategy or retrieve the token separately.

Is nonce verification alone enough to protect an AJAX or REST endpoint?

No. The server must also verify authentication, the user's capability, and the validity and acceptability of the input data. A nonce is not a replacement for authorization.

Where should a nonce be passed in the WordPress REST API?

For requests using the relevant session, WordPress typically uses the `X-WP-Nonce` HTTP header. Do not store this token in long-lived cached HTML or treat it as a universal API key.

Conclusion

Caching and WordPress nonces work well together only when public content is separated from personalized data. HTML containing session-based or time-sensitive tokens should not be distributed indiscriminately to all visitors through a page cache.

The optimal setup is to cache static content, exclude personalized pages, retrieve the nonce through a separate uncached request, and verify not only the nonce but also the user's permissions on the server. This preserves the benefits of caching without 403 errors or weakened security.

← All WordPress materials