Skip to main content
Robert Michalski

Protecting WordPress admin AJAX actions

Use cases of WordPress AJAX requests #

AJAX requests are used to get data dynamically on WordPress websites, without reloading the page.

For front-end purposes, when calling AJAX actions from the WordPress admin back-end, a nonce token can be passed along with the request and checked using check_ajax_referer function in the handler.

Ajax requests can also be used to fetch data from WordPress from another system, for instance an API or processing script. Then it becomes important to secure the AJAX action so it can't be accessed by just anyone.

Example of an AJAX action that needs to be protected.

// https://yourwebsite.com/wp-admin/admin-ajax.php?action=get_custom_data&param1=value1
add_action( 'wp_ajax_nopriv_get_custom_data', 'YOUR_PREFIX_ajax_get_custom_data' ); // anyone can call this, use for internal servers
add_action( 'wp_ajax_get_custom_data', 'YOUR_PREFIX_ajax_get_custom_data' ); // only logged in users can call this

function YOUR_PREFIX_ajax_get_custom_data() {
    // get params
    $param1 = $_REQUEST['param1'];
    // get data
    $data = ...

    header('Content-Type: application/json');
    die(json_encode($data));
}

How to protect an AJAX request using IP allow-list #

One way to protect an AJAX end-point is to only permit connections from IPs in an allow-list. It's not really secure to protect routes based on IP, because the IP of a request can be spoofed. However, it's better than leaving it wide open, add internal IPs of your servers to an array and check if the visitors IP is in the array. Check this post on how to get the IP from a request in PHP and check if it's in allowed access.

function YOUR_PREFIX_is_call_from_internal_server() {
    $allowed_ips = [
        '123.234.56.7',
        ...
    ];
    $ip = get_ip_address_of_visitor(); // check post mentioned above for this code
    return in_array($ip, $allowed_ips, true);
}

function YOUR_PREFIX_ajax_get_custom_data() {
    if (!YOUR_PREFIX_is_call_from_internal_server()) {
        status_header(401);
        nocache_headers(); // adds headers to prevent browser and proxies to cache result
        die();
    }

    // get params
    $param1 = $_REQUEST['param1'];
    // get data
    $data = ...

    header('Content-Type: application/json');
    die(json_encode($data));
}

How to protect an AJAX request using a shared secret token #

A more secure way to protect an AJAX end-point is to check for a secret token in the headers of the request. The caller includes a secret token in the "Authorization" or a custom header like "X-Secret-Token", which is checked in the AJAX handler.

function YOUR_PREFIX_is_call_with_secret() {
    $auth_header = $_SERVER['HTTP_AUTHORIZATION'];
    //$auth_header = $_SERVER['HTTP_X-SECRET-TOKEN'];
    $server_key = 'your-super-secret-very-long-random-token'; // preferably get this from .env file or a defined constant, hard-coding magic values is bad practice
    return is_string($auth_header) &&
        is_string($server_key) &&
        str_replace('Bearer ', '', $auth_header) === $server_key; // Authorization: Bearer ... is a standard way to pass tokens, this will remove "Bearer " so the token can be checked properly
}

function YOUR_PREFIX_ajax_get_custom_data() {
    if (!YOUR_PREFIX_is_call_with_secret()) {
        status_header(401);
        nocache_headers(); // adds headers to prevent browser and proxies to cache result
        die();
    }

    // get params
    $param1 = $_REQUEST['param1'];
    // get data
    $data = ...

    header('Content-Type: application/json');
    die(json_encode($data));
}