How to protect AJAX and REST with nonces in PHP and JS in WordPress

Contents

Introduction

This tutorial explains, in full detail, how to protect WordPress AJAX and REST API requests using nonces from both PHP and JavaScript. You will learn how WordPress nonces work, how to generate and pass them to client-side code, how to verify them on the server, best practices, debugging tips, and the limitations you must be aware of. Concrete, copy-pasteable examples are provided for enqueueing scripts, sending secure requests via fetch/XHR, handling AJAX via admin-ajax.php, and registering REST routes with permission checks.

What a WordPress nonce is (and is not)

A WordPress nonce is a token used to help protect URLs and forms from certain types of misuse, primarily Cross-Site Request Forgery (CSRF). Important facts:

  • Action-specific: A nonce is tied to a specific action string (for example my_action or wp_rest).
  • User-specific: Nonces are tied to the user (or to user ID 0 for unauthenticated contexts).
  • Time-limited: By default nonces expire after 12 hours (filterable via nonce_life).
  • Not a full authentication mechanism: Nonces prevent CSRF but do not substitute for proper authentication and capability checks. They are not cryptographically single-use tokens.

Key WordPress functions

wp_create_nonce(action) Generate a nonce string for the given action.
wp_verify_nonce(nonce, action) Return 1 or 2 on success (dependent on age) or false on failure. Use to verify a nonce manually.
check_ajax_referer(action, query_arg, die) Convenience for AJAX handlers. Checks a nonce and dies (by default) with -1 on failure. Returns integer on success.
wp_send_json_success / wp_send_json_error Send JSON response with appropriate headers for AJAX calls.
register_rest_route(…, permission_callback) Best place to verify nonces and capabilities for custom REST endpoints.

High-level flow

  1. Generate a nonce in PHP with wp_create_nonce() and pass it to JavaScript via wp_localize_script() or wp_add_inline_script().
  2. From JavaScript include the nonce with every AJAX or REST request (POST/PUT/DELETE): for admin-ajax.php requests as a parameter (often named security), for REST endpoints as the X-WP-Nonce header (or _wpnonce param where appropriate).
  3. On the server verify the nonce with check_ajax_referer() for admin-ajax or with wp_verify_nonce() (or other checks) inside your REST permission_callback. Also check the current users capabilities to authorize the action.
  4. Return appropriate error responses when verification fails and sanitize/validate all inputs.

Step-by-step examples

1) Enqueueing your script and providing nonces to JS

Recommended approach is to enqueue your script and provide both the AJAX URL and a nonce. For REST-based requests, provide the REST root and the REST nonce.

functions.php (enqueue and localize):

 admin_url( admin-ajax.php ),
            ajax_nonce => ajax_nonce,
            rest_root  => esc_url_raw( rest_url() ),
            rest_nonce => rest_nonce,
        )
    )
}
?>

2) AJAX (admin-ajax.php) from JavaScript

For admin-ajax, include the nonce in the POST body under the name you will check (commonly security). Heres an example using fetch and URLSearchParams. You can use jQuery.ajax similarly.

JavaScript example (myplugin-frontend.js):

// Assume MyPluginData.ajax_url and MyPluginData.ajax_nonce are available
function sendMyAjax(dataObj) {
    const body = new URLSearchParams()
    body.append(action, myplugin_do_something) // required by admin-ajax
    body.append(security, MyPluginData.ajax_nonce) // nonce param
    // append your other data:
    Object.keys(dataObj).forEach(key => body.append(key, dataObj[key]))

    return fetch(MyPluginData.ajax_url, {
        method: POST,
        headers: {
            Content-Type: application/x-www-form-urlencoded charset=UTF-8
        },
        body: body.toString(),
        credentials: same-origin
    }).then(response => response.json())
}

// Usage:
sendMyAjax({ foo: bar }).then(response => {
    if (response.success) {
        console.log(Success:, response.data)
    } else {
        console.error(AJAX error:, response)
    }
})

3) PHP handler for admin-ajax.php

In PHP, use check_ajax_referer() to verify the nonce and then perform the action. Use appropriate capability checks for the current user.

 Insufficient permissions ), 403 )
    }

    // Sanitize and use incoming data:
    foo = isset( _POST[foo] ) ? sanitize_text_field( wp_unslash( _POST[foo] ) ) : 

    // Do the work...
    result = array( received => foo )

    wp_send_json_success( result )
}
?>

4) REST API: enqueueing REST root and REST nonce

For REST requests use the X-WP-Nonce header and generate a nonce with the action wp_rest. The REST nonce is typically created with wp_create_nonce(wp_rest) and verified on the server with wp_verify_nonce() in a permission callback.

functions.php (localize REST data):

 esc_url_raw( rest_url() ),   // example: https://example.com/wp-json/
            nonce => wp_create_nonce( wp_rest )
        )
    )
}
?>

5) JavaScript: sending REST requests with X-WP-Nonce

Use the REST root and include the X-WP-Nonce header. This is the standard WordPress method for cookie-based authentication on the REST API.

// Example: POST to a custom REST route myplugin/v1/do
const url = MyPluginREST.root   myplugin/v1/do
fetch(url, {
    method: POST,
    headers: {
        Content-Type: application/json,
        X-WP-Nonce: MyPluginREST.nonce
    },
    credentials: same-origin, // important for cookie auth
    body: JSON.stringify({ foo: bar })
})
.then(res => {
    if (!res.ok) throw res
    return res.json()
})
.then(data => console.log(REST success, data))
.catch(err => {
    console.error(REST error, err)
})

6) Registering a REST route and verifying the nonce

Always use the permission_callback parameter for verification. Verify both the nonce and the capability you require for the action. Return a WP_Error instance with a proper status code (403 for forbidden) on failure.

 POST,
        callback            => myplugin_rest_do,
        permission_callback => myplugin_rest_permission_check,
    ) )
} )

function myplugin_rest_permission_check( request ) {
    // Get X-WP-Nonce header
    nonce = request->get_header( X-WP-Nonce )

    if ( empty( nonce )  ! wp_verify_nonce( nonce, wp_rest ) ) {
        return new WP_Error( rest_forbidden, Invalid nonce, array( status => 403 ) )
    }

    // Optional: check capability or role
    if ( ! current_user_can( edit_posts ) ) {
        return new WP_Error( rest_forbidden, Insufficient permissions, array( status => 403 ) )
    }

    return true
}

function myplugin_rest_do( WP_REST_Request request ) {
    params = request->get_json_params()
    foo = isset( params[foo] ) ? sanitize_text_field( params[foo] ) : 

    // Do the action...
    return rest_ensure_response( array( received => foo ) )
}
?>

Common patterns, gotchas and debugging tips

  • admin-ajax vs REST: admin-ajax uses action param and check_ajax_referer(). REST uses X-WP-Nonce header and permission_callback. Prefer REST for new code when possible.
  • Headers and same-origin: X-WP-Nonce only works for same-origin requests (cookie-based auth). If you call from a completely different origin you will need other auth (JWT, OAuth, application passwords).
  • Handling failures: check_ajax_referer() dies by default. Use the third parameter false to handle errors gracefully and return JSON errors instead.
  • Nonce name consistency: The action name used in wp_create_nonce() must match the one used in verification. For REST use the conventional wp_rest. For AJAX use a custom action string such as myplugin_ajax_action and pass the same name to check_ajax_referer().
  • Nonce aging: wp_verify_nonce() may return 1 or 2 depending on age treat any non-false value as valid. Use the nonce_life filter to change the duration (rarely necessary).
  • Unauthenticated (wp_ajax_nopriv): You can create and verify nonces for unauthenticated requests (user ID 0), but this is inherently less secure than protecting actions for authenticated users. For truly public endpoints consider other protections (rate limiting, captchas, server-side validation).
  • REST permission_callback vs callback: Never do capability checks inside the main callback only. Use permission_callback to block unauthorized requests before running the main callback.
  • Sanitize inputs: Nonces prevent CSRF but not malicious input. Always sanitize and escape user input.

Advanced topics

Customizing nonce life

To change the default nonce lifetime (12 hours), use the nonce_life filter. Use caution: reducing time increases friction increasing time weakens the time-bound property.


Handling check_ajax_referer() without dying

If you want to return structured JSON errors rather than WordPress dying with -1, set the third parameter to false and handle failure yourself.

 Invalid nonce ), 403 )
    }

    // proceed...
}
?>

Alternatives for non-logged-in REST endpoints

If your REST endpoint must accept requests from unauthenticated third parties, use a different authentication mechanism:

  • Application Passwords (core feature for basic auth over HTTPS)
  • OAuth or JWT tokens (plugins available)
  • Custom API keys validated and rate-limited server-side

Checklist before shipping: security best practices

  1. Use wp_create_nonce() and pass it to JavaScript via wp_localize_script() or wp_add_inline_script().
  2. Always include the nonce in requests: as security for admin-ajax, and as X-WP-Nonce for REST.
  3. Verify nonces server-side with check_ajax_referer() for admin-ajax or wp_verify_nonce() inside a REST permission_callback.
  4. Also verify user capabilities with current_user_can() in the permission step.
  5. Sanitize and validate all input escape output.
  6. Use credentials: same-origin on fetch so cookies are included for cookie-based REST auth.
  7. Use HTTPS for all authenticated requests to protect cookies and nonces in transit.

Troubleshooting common errors

  • Response -1 from admin-ajax: Nonce check failed. Make sure the nonce was sent and that the action string matches.
  • 403 from REST: Permission callback failed. Inspect request headers to ensure X-WP-Nonce header was present and correct. Also check user login state.
  • Missing wp_localize_script data: Ensure the script handle name used in wp_localize_script matches the registered/enqueued script handle.
  • CORS issues: REST X-WP-Nonce uses cookie authentication cross-origin requests will not include cookies unless CORS is configured and credentials allowed. For remote clients use other auth methods.

Full minimal end-to-end example summary

Below is a compact recap of the core pieces: enqueue/localize, front-end fetch, admin-ajax handler, REST registration with permission callback. These snippets are minimal but complete.

Enqueue localize (PHP):

 admin_url( admin-ajax.php ),
        ajax_nonce => wp_create_nonce( myplugin_ajax_action ),
        rest_root => rest_url(),
        rest_nonce => wp_create_nonce( wp_rest ),
    ) )
} )
?>

Front-end fetch examples (JS):

// AJAX
fetch(MyPlugin.ajax_url, {
  method: POST,
  headers: { Content-Type: application/x-www-form-urlencoded },
  body: new URLSearchParams({ action: my_action, security: MyPlugin.ajax_nonce, foo: bar }),
  credentials: same-origin
}).then(r => r.json()).then(console.log)

// REST
fetch(MyPlugin.rest_root   myplugin/v1/do, {
  method: POST,
  headers: { Content-Type: application/json, X-WP-Nonce: MyPlugin.rest_nonce },
  credentials: same-origin,
  body: JSON.stringify({ foo: bar })
}).then(r => r.json()).then(console.log)

AJAX handler (PHP):

 Invalid nonce ), 403 )
    }
    foo = isset( _POST[foo] ) ? sanitize_text_field( wp_unslash( _POST[foo] ) ) : 
    wp_send_json_success( array( received => foo ) )
}
?>

REST registration (PHP):

 POST,
        callback => function( WP_REST_Request request ) {
            params = request->get_json_params()
            foo = isset( params[foo] ) ? sanitize_text_field( params[foo] ) : 
            return rest_ensure_response( array( received => foo ) )
        },
        permission_callback => function( request ) {
            nonce = request->get_header( X-WP-Nonce )
            if ( ! nonce  ! wp_verify_nonce( nonce, wp_rest ) ) {
                return new WP_Error( rest_forbidden, Invalid nonce, array( status => 403 ) )
            }
            if ( ! current_user_can( edit_posts ) ) {
                return new WP_Error( rest_forbidden, Insufficient permissions, array( status => 403 ) )
            }
            return true
        }
    ) )
} )
?>

Final notes — when not to rely on nonces

Nonces are excellent for mitigating CSRF when using cookie-based authentication with logged-in users, but they are not a substitute for:

  • Strong authentication for API clients (use Application Passwords, OAuth, or JWT for third-party integrations).
  • Capability checks (always call current_user_can() where appropriate).
  • Transport security — always use HTTPS in production.

References



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *