WordPress nonces provide essential CSRF protection for your site. Learn how to use wp_create_nonce and verify requests to keep your forms and AJAX secure.
Last month, I spent about two days auditing a client’s custom plugin because users were reporting weird data shifts in their profiles. It turned out the forms lacked any verification, meaning a malicious site could trick an authenticated admin into submitting requests on their behalf. That’s a classic Cross-Site Request Forgery (CSRF) attack, and WordPress nonces are your first line of defense against it.
A nonce stands for "number used once." In the WordPress ecosystem, it’s a unique token generated by the server to ensure that a request originated from your site, not a third-party attacker. It isn't a silver bullet for total security—like CSRF protection that you understand and can implement today explains—but it’s an essential layer for validating user intent.
Crucially, nonces are tied to the user session and a specific action. If an attacker tries to replicate your form submission, their token won't match the one WordPress expects for that specific user and task.
When you build a form, you need to inject a hidden field that holds the token. You use wp_nonce_field() for this.
PHP<form method="post" action="process-data.php"> <?php wp_nonce_field('my_custom_action', 'my_nonce_field'); ?> <!-- Your form inputs here --> <input type="submit" value="Submit"> </form>
The first argument, 'my_custom_action', is an arbitrary string that acts as a namespace. Always make this specific to the action being performed. On the server side, you verify it like this:
PHPif (isset($_POST['my_nonce_field']) && wp_verify_nonce($_POST['my_nonce_field'], 'my_custom_action')) { #6A9955">// Process your data safely } else { wp_die('Invalid nonce. Security check failed.'); }
If you're building a modern interface, you're likely using WordPress AJAX. This is where things get tricky. You can't just drop a hidden field into a static HTML form because the request is triggered via JavaScript.
We first tried passing the nonce as a global JavaScript variable using wp_localize_script(). It worked, but it felt messy to manage dozens of global variables for different features. A cleaner approach is to use a data attribute on the trigger element or a specific meta tag in the header.
Here is the standard workflow:
Generate the token in PHP:
PHP$nonce = wp_create_nonce('ajax_action_name'); wp_localize_script('my-script-handle', 'wpApiSettings', [ 'nonce' => $nonce ]);
Send it in your AJAX request:
JAVASCRIPTfetch(CE9178">'/wp-admin/admin-ajax.php', { method: CE9178">'POST', body: new URLSearchParams({ action: CE9178">'my_ajax_handler', nonce: wpApiSettings.nonce }) });
Verify it in your handler:
PHPcheck_ajax_referer('ajax_action_name', 'nonce');
The check_ajax_referer() function is a helper that wraps wp_verify_nonce() and automatically calls wp_die() if the check fails. It saves you a few lines of boilerplate code and ensures you don't forget to handle the failure case.
delete_post, they shouldn't be able to use it for update_user_email.current_user_can('edit_posts').Are nonces enough to stop all CSRF? No. They prevent an attacker from tricking a user into submitting a form. They don't protect against SQL injection (use WordPress Database Queries: Securely Using $wpdb and Preparing SQL for that) or XSS.
Can I use the same nonce for every request? Technically, yes, but you shouldn't. Using the same nonce for multiple actions increases the window of opportunity for an attacker to hijack a request.
What happens if the nonce expires?
The wp_verify_nonce() function will return false. Your code should handle this gracefully by prompting the user to refresh the page or re-submit the form, rather than just showing a broken interface.
Implementing WordPress security via nonces is a habit that separates hobbyists from professionals. It’s annoying to add these checks every single time, but the payoff is a site that doesn't get compromised because of a simple form submission. Next time, I might look into automating nonce injection for all forms via a base class, but for now, I prefer the explicit approach—it's easier to debug when something goes wrong.
The WordPress user object acts as the engine for authentication and authorization. Learn how to leverage the WP_User class to manage roles and capabilities.
Read moreWordPress plugin activation happens in a specific sequence. Learn how to use register_activation_hook to handle setup tasks during the development lifecycle.