In a blog post from Wordfence, they explained the importance of verifying both authorization and intent when writing WordPress functions that perform sensitive work.
I wanted to summarize the idea here for future reference, because it is one of those security patterns that should become automatic when building WordPress features.
The Problem: Bad Actors Running Scripts
When an action is open too wide, an attacker may be able to access it through an AJAX call or another crafted request.
If the function does not check whether the current user is allowed to perform the action, and does not check whether the request came from an intentional trusted interaction, the function may run when it should not.
For example, imagine an AJAX action that deletes customer records. If it is registered and callable, but does not check permissions or intent, an attacker may try to trigger it directly.
The Solution: Verify Authorization and Intent
There are two checks that should usually happen before sensitive WordPress actions run:
- Authorization: Is the current user allowed to perform this action?
- Intent: Did this request come from a legitimate page or interaction?
Authorization is commonly checked with current_user_can(). Intent is commonly checked with a nonce.
Do Not Do This
This is the kind of unsafe pattern you want to avoid:
<?php
function twd_delete_all_customers(): void {
delete_all_customers();
}
add_action( 'wp_ajax_delete_customers', 'twd_delete_all_customers' );
That function runs the destructive action without checking who is making the request or whether the request was intentional.
Better: Check Capability and Nonce
Here is a safer modern AJAX pattern:
<?php
/**
* Handle a sensitive admin-only AJAX action.
*/
function twd_delete_all_customers_ajax(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error(
[ 'message' => __( 'You are not allowed to perform this action.', 'your-textdomain' ) ],
403
);
}
check_ajax_referer( 'twd_delete_customers', 'nonce' );
// Run the sensitive operation only after both checks pass.
delete_all_customers();
wp_send_json_success(
[ 'message' => __( 'Customers deleted successfully.', 'your-textdomain' ) ]
);
}
add_action( 'wp_ajax_twd_delete_customers', 'twd_delete_all_customers_ajax' );
In this example, current_user_can( 'manage_options' ) checks whether the logged-in user has an admin-level capability. The check_ajax_referer() call verifies the nonce sent with the AJAX request.
Creating and Sending the Nonce
You can create the nonce when enqueueing your script and pass it to JavaScript with wp_localize_script() or wp_add_inline_script().
<?php
function twd_enqueue_admin_scripts( string $hook_suffix ): void {
wp_enqueue_script(
'twd-admin',
plugin_dir_url( __FILE__ ) . 'admin.js',
[ 'jquery' ],
'1.0.0',
true
);
wp_localize_script(
'twd-admin',
'twdAdmin',
[
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'twd_delete_customers' ),
]
);
}
add_action( 'admin_enqueue_scripts', 'twd_enqueue_admin_scripts' );
Then your JavaScript can send the nonce with the AJAX request:
jQuery(function ($) {
$('.delete-customers-button').on('click', function () {
if (!window.confirm('Are you sure you want to delete these customers?')) {
return;
}
$.post(twdAdmin.ajaxUrl, {
action: 'twd_delete_customers',
nonce: twdAdmin.nonce
}).done(function (response) {
console.log(response);
});
});
});
Why Both Checks Matter
A nonce is not a permission check. WordPress specifically warns that nonces should not be relied on for authentication, authorization, or access control.
A nonce helps verify intent. A capability check verifies authorization.
You need both because each protects against a different problem:
- If you only check the nonce, a user without the right permission may still be able to trigger the action if they get access to a valid nonce.
- If you only check the capability, a malicious site may be able to trick a logged-in authorized user into making an unintended request.
For sensitive actions, especially destructive actions, check the user's capability and verify the nonce before doing the work.
Final Thought
The habit is simple: before a WordPress function changes data, deletes data, exports data, sends emails, updates settings, or performs any sensitive action, verify authorization and intent.