AJAX requests in WordPress are handled through the admin-ajax.php endpoint, which is accessible to any visitor, making proper authentication and input validation essential for every handler you register. A Cross-Site Request Forgery (CSRF) attack tricks an authenticated user’s browser into sending a forged request to a site they are already logged into. WordPress nonces — one-time tokens tied to a specific action and user — are the standard mechanism for preventing CSRF in AJAX handlers. The wp_create_nonce() function generates a nonce on the server and check_ajax_referer() verifies it before any processing occurs. Input sanitization is equally important: every value from $_POST or $_GET must be treated as untrusted until validated and sanitized. WordPress ships a rich set of sanitization helpers including sanitize_text_field(), absint(), sanitize_email(), and wp_kses(). The wp_unslash() wrapper should be applied before sanitization to strip magic-quote slashes that PHP may add on older servers. Returning data with wp_send_json_success() and wp_send_json_error() sets the correct Content-Type header and terminates execution automatically. Registering both wp_ajax_{action} and wp_ajax_nopriv_{action} hooks controls whether the endpoint is available only to logged-in users or to everyone. The structured data guide demonstrates another server-side PHP pattern that touches public output. See the WooCommerce access control post for a real-world example of role checks combined with sanitized input. Applying these patterns consistently across all AJAX handlers eliminates the most common class of vulnerabilities in custom WordPress code.
Problem: Custom WordPress AJAX handlers that skip nonce checks and raw input sanitization are vulnerable to CSRF attacks and data injection from untrusted requests.
Solution: Use wp_localize_script() to pass a nonce to JavaScript, call check_ajax_referer() at the top of each handler, and sanitize every $_POST value with the appropriate WordPress sanitization function before use.
// Enqueue script with a nonce for AJAX
add_action('wp_enqueue_scripts', function() {
wp_enqueue_script('my-ajax', get_template_directory_uri() . '/js/ajax.js', ['jquery'], '1.0', true);
wp_localize_script('my-ajax', 'myAjax', [
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_nonce'),
]);
});
// Register AJAX handler for logged-in and guest users
add_action('wp_ajax_my_action', 'handle_my_ajax');
add_action('wp_ajax_nopriv_my_action', 'handle_my_ajax');
function handle_my_ajax() {
check_ajax_referer('my_ajax_nonce', 'nonce');
$search = sanitize_text_field(wp_unslash($_POST['search'] ?? ''));
$results = get_posts(['s' => $search, 'posts_per_page' => 10, 'post_status' => 'publish']);
wp_send_json_success(array_map(function($post) {
return [
'id' => $post->ID,
'title' => get_the_title($post),
'url' => get_permalink($post),
];
}, $results));
}
NOTE: Set the second parameter of check_ajax_referer() to the exact field name that holds the nonce in the POST data — mismatched names silently fail verification and leave the handler unprotected.