Securing WordPress AJAX Requests
Protect WordPress AJAX endpoints with nonce verification, capability checks, and input validation.
AJAX requests in WordPress require proper security controls. Without them, attackers can exploit your endpoints to manipulate data or gain unauthorized access.
AJAX Security Fundamentals
Security Requirements
- Nonce verification
- Capability checks
- Input validation
- Output sanitization
- Rate limiting
Nonce Implementation
Creating and Passing Nonces
// PHP: Localize script with nonce
wp_localize_script('my-ajax-script', 'myAjax', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('my_ajax_action')
));
// JavaScript: Include nonce in request
jQuery.ajax({
url: myAjax.ajaxurl,
type: 'POST',
data: {
action: 'my_action',
security: myAjax.nonce,
data: myData
},
success: function(response) {
// Handle response
}
});
Verifying Nonces
// PHP: AJAX handler
add_action('wp_ajax_my_action', 'handle_my_ajax_action');
add_action('wp_ajax_nopriv_my_action', 'handle_my_ajax_action'); // For non-logged-in
function handle_my_ajax_action() {
// Verify nonce FIRST
if (!check_ajax_referer('my_ajax_action', 'security', false)) {
wp_send_json_error('Invalid security token', 403);
}
// Process request...
wp_send_json_success($result);
}
Capability Verification
Check User Permissions
add_action('wp_ajax_admin_action', 'handle_admin_ajax');
function handle_admin_ajax() {
// Verify nonce
check_ajax_referer('admin_action_nonce', 'security');
// Verify capabilities
if (!current_user_can('manage_options')) {
wp_send_json_error('Insufficient permissions', 403);
}
// Safe to proceed
$result = do_admin_thing();
wp_send_json_success($result);
}
// For post-specific actions
function handle_edit_post_ajax() {
check_ajax_referer('edit_post_nonce', 'security');
$post_id = absint($_POST['post_id']);
if (!current_user_can('edit_post', $post_id)) {
wp_send_json_error('Cannot edit this post', 403);
}
// Continue...
}
Input Validation
Sanitize All Input
function handle_form_ajax() {
check_ajax_referer('form_nonce', 'security');
// Validate and sanitize each field
$email = sanitize_email($_POST['email'] ?? '');
$name = sanitize_text_field($_POST['name'] ?? '');
$message = sanitize_textarea_field($_POST['message'] ?? '');
$post_id = absint($_POST['post_id'] ?? 0);
// Validate required fields
if (empty($email) || !is_email($email)) {
wp_send_json_error('Invalid email address');
}
if (empty($name)) {
wp_send_json_error('Name is required');
}
// Validate post exists
if ($post_id && !get_post($post_id)) {
wp_send_json_error('Invalid post ID');
}
// Process validated data...
}
Output Encoding
Escape Response Data
function handle_data_ajax() {
check_ajax_referer('data_nonce', 'security');
$posts = get_posts(array('numberposts' => 5));
$response = array();
foreach ($posts as $post) {
$response[] = array(
'id' => $post->ID,
'title' => esc_html($post->post_title),
'link' => esc_url(get_permalink($post->ID)),
'excerpt' => esc_html(get_the_excerpt($post))
);
}
wp_send_json_success($response);
}
Rate Limiting AJAX
Prevent Abuse
function handle_rate_limited_ajax() {
check_ajax_referer('action_nonce', 'security');
$user_id = get_current_user_id();
$ip = $_SERVER['REMOTE_ADDR'];
$key = $user_id ? 'ajax_user_' . $user_id : 'ajax_ip_' . md5($ip);
$count = get_transient($key) ?: 0;
if ($count >= 10) { // 10 requests per minute
wp_send_json_error('Rate limit exceeded', 429);
}
set_transient($key, $count + 1, 60);
// Process request...
}
Complete Secure Handler
Full Example
add_action('wp_ajax_secure_action', 'wpfs_secure_ajax_handler');
function wpfs_secure_ajax_handler() {
// 1. Verify nonce
if (!check_ajax_referer('secure_action_nonce', 'security', false)) {
wp_send_json_error(array(
'message' => 'Security verification failed'
), 403);
}
// 2. Check capabilities
if (!current_user_can('edit_posts')) {
wp_send_json_error(array(
'message' => 'Insufficient permissions'
), 403);
}
// 3. Rate limit
$user_id = get_current_user_id();
if (!wpfs_check_rate_limit('ajax_' . $user_id, 20, 60)) {
wp_send_json_error(array(
'message' => 'Too many requests'
), 429);
}
// 4. Validate input
$post_id = absint($_POST['post_id'] ?? 0);
$action_type = sanitize_text_field($_POST['action_type'] ?? '');
if (!$post_id || !in_array($action_type, array('publish', 'draft'))) {
wp_send_json_error(array(
'message' => 'Invalid parameters'
), 400);
}
// 5. Verify ownership
if (!current_user_can('edit_post', $post_id)) {
wp_send_json_error(array(
'message' => 'Cannot edit this post'
), 403);
}
// 6. Perform action
$result = wp_update_post(array(
'ID' => $post_id,
'post_status' => $action_type
));
if (is_wp_error($result)) {
wp_send_json_error(array(
'message' => $result->get_error_message()
), 500);
}
// 7. Return success
wp_send_json_success(array(
'message' => 'Post updated successfully',
'post_id' => $post_id,
'status' => $action_type
));
}
Security Checklist
- [ ] Nonce created and passed to JavaScript
- [ ] Nonce verified in handler
- [ ] User capabilities checked
- [ ] All input sanitized
- [ ] Output escaped
- [ ] Rate limiting implemented
- [ ] Error messages don't leak info
Conclusion
Secure AJAX requires nonces, capability checks, input validation, and rate limiting at minimum. Always verify permissions before processing requests and sanitize all user input.
Written by Sarah Chen
WP Folder Shield Team