By default, the WordPress REST API is enabled, and this can cause your website to leak user contact data. Here we’re going to disable access to the REST API, without using a plugin. To see what’s going on with the REST API, try the following URL in a browser (replace www.example.org with your website’s domain):
https://www.example.org/wp-json/wp/v2/users
You can actually use a ready-made plugin to disable the WordPress REST API. That’s a bit too easy though, so… to avoid having lots of plugins loaded, we’re just going to add a little bit of code to our theme’s functions.php file.
We’re going to introduce several ways to affect the REST API:
- Disable the REST API completely.
- Only allow the REST API for logged-in users and whitelisted IP addresses.
- Make the REST API available to everyone (like how WordPress works normally) but disable user enumeration.
- If the REST API is not available (or the endpoint is denied) instead of showing an empty page we’ll redirect the user to your proper 404 page. It’s a nice detail 😎
- As an added bones, we can write to the system auth log so that Fail2Ban can block the remote IP address at firewall-level 😎😎
Let’s Write the Code
First, create a new file in your custom child theme’s folder called functions-rest-api.php. This will let us keep our new REST API functions separate from anything else you’ve got going on in your main functions.php file. It also makes it easy to reuse these new tools on other websites.
Paste the following into the new functions-rest-api.php file.
<?php /** * * https://wp-tutorials.tech/optimise-wordpress/disable-wordpress-rest-api-without-a-plugin/ * * By default, the REST API is enabled - like the default WordPress behaviour. * * If you want to disable the API completely for non-logged-in users then just * call hw_completely_disable_rest_api(), BUT BUT BUT, you probably just want * to add this to your functions.php * * define('HW_IS_REST_API_USER_ENUMERATION_DISABLED', true); * * If you want to only allow REST API calls for non-logged-in users from * certain IP addresses, just pass those IP addresses in the constant * HW_REST_API_IP_WHITELIST, like this: * * define( * 'HW_REST_API_IP_WHITELIST', * array('127.0.0.1', '::1', 'address-1', 'address-2', '...') * ); * * If you want to write events to the system auth log for Fail2Ban to see, then: * * define('HW_REST_IS_FAIL2BAN_ENABLED', true); * */ // Block direct access. defined('WPINC') || die(); define('HW_REST_API_DEFAULT_IP_WHITELIST', array('127.0.0.1', '::1')); function hw_completely_disable_rest_api() { if (!defined('HW_IS_REST_API_DISABLED')) { define('HW_IS_REST_API_DISABLED', true); } } function hw_disable_rest_api_user_enumeration() { if (!defined('HW_IS_REST_API_USER_ENUMERATION_DISABLED')) { define('HW_IS_REST_API_USER_ENUMERATION_DISABLED', true); } } function hw_request_block_ip($ip_address = '') { if (empty($ip_address)) { $ip_address = sanitize_text_field($_SERVER['REMOTE_ADDR']); } if (!empty($ip_address)) { // error_log('Blocking IP: ' . $ip_address); $is_fail2ban_enabled = true; if (defined('HW_REST_IS_FAIL2BAN_ENABLED') && (HW_REST_IS_FAIL2BAN_ENABLED !== true)) { $is_fail2ban_enabled = false; } if ($is_fail2ban_enabled) { openlog('wp(' . sanitize_text_field($_SERVER['HTTP_HOST']) . ')', LOG_NDELAY | LOG_PID, LOG_AUTH); syslog(LOG_INFO, "REST User Enum " . $ip_address); closelog(); } else { error_log('Found enumeration attempt, but HW_REST_IS_FAIL2BAN_ENABLED is not enabled.'); } } } function hw_rest_api_init() { $is_rest_api_available = true; $is_attempting_user_enumeration = false; $is_user_an_administrator = false; if ($is_user_authenticated = is_user_logged_in()) { $is_user_an_administrator = current_user_can('administrator'); } $is_user_authorised = ($is_user_authenticated && $is_user_an_administrator); $is_remote_ip_whitelisted = false; if (defined('HW_REST_API_IP_WHITELIST') && is_array(HW_REST_API_IP_WHITELIST)) { $is_remote_ip_whitelisted = in_array($_SERVER['REMOTE_ADDR'], HW_REST_API_IP_WHITELIST); } elseif (defined('HW_REST_API_DEFAULT_IP_WHITELIST') && is_array(HW_REST_API_DEFAULT_IP_WHITELIST)) { $is_remote_ip_whitelisted = in_array($_SERVER['REMOTE_ADDR'], HW_REST_API_DEFAULT_IP_WHITELIST); } else { // ... } $is_ip_block_requested = false; $is_rest_api_disabled = false; if (defined('HW_IS_REST_API_DISABLED')) { $is_rest_api_disabled = (HW_IS_REST_API_DISABLED === true); } $is_public_user_enumeration_disabled = true; if (defined('HW_IS_REST_API_USER_ENUMERATION_DISABLED') && (HW_IS_REST_API_USER_ENUMERATION_DISABLED !== true)) { $is_public_user_enumeration_disabled = false; } $is_endpoint_blocked = false; if (!$is_user_authenticated && !$is_remote_ip_whitelisted && $is_public_user_enumeration_disabled) { $prefix = rest_get_url_prefix(); $users_path = '/' . $prefix . '/wp/v2/users'; if ((isset($_SERVER['REQUEST_URI']) && (strpos($_SERVER['REQUEST_URI'], $users_path) !== false)) || (isset($_REQUEST['rest_route']) && (strpos($_SERVER['rest_route'], $users_path) !== false)) ) { $is_endpoint_blocked = true; $is_ip_block_requested = true; } } $http_error_code = null; $is_rest_api_available = false; if ($is_user_authorised) { // ... } elseif ($is_remote_ip_whitelisted) { // ... } elseif ($is_rest_api_disabled) { $http_error_code = 404; } elseif (!$is_endpoint_blocked) { // ... } else { $http_error_code = 404; } if ($is_ip_block_requested) { hw_request_block_ip(); } if ($http_error_code == 404) { header("Status: 404 Not Found"); $GLOBALS['wp_query']->set_404(); status_header(404); nocache_headers(); include get_query_template('404'); exit; } elseif (!empty($http_error_code)) { http_response_code($http_error_code); die('ERR: ' . $http_error_code); } else { // OK. } } add_action('rest_api_init', 'hw_rest_api_init', 100);
Straight away you should see we’ve got a few new functions we can use in our code:
- hw_completely_disable_rest_api()
- hw_disable_rest_api_user_enumeration()
We’ll come back to these in a moment. First, let’s reference our new code from the main functions.php file. Add the following code somewhere near the top of functions.php in your custom child theme.
// Load functions to adjust visibility of the REST API. require_once dirname(__FILE__) . '/functions-rest-api.php';
Now we’re ready to call the code and run some tests to prove it’s working.
Using the New Functions
The code is pretty easy to use. If you want to disable the REST API completely, just add the following somewhere in your functions.php file.
/** * Completely disable the REST API for non-logged-in users. * Logged-in users will still have access to the API. */ hw_completely_disable_rest_api();
You can test this by putting the following URL into your browser.
https://www.example.org/wp-json/wp/v2/users
If everything’s working properly then should be directed to your site’s 404 Not Found page.
BUT, BUT, BUT. You probably don’t want to do this. If you’ve got Contact Form 7 installed on your website then this might break the forms. So, this is where we need to be a bit more surgical. Instead of calling hw_completely_disable_rest_api(), try adding the following instead:
/** * Disable user-enumeration in the REST API. Either of these methods * should work fine. */ hw_disable_rest_api_user_enumeration();
Now try going to the user-enumeration URL and you should see a different error message. In fact, you should get a 404 Not Found response, because our hw_rest_endpoints() filter-handler function has removed the endpoints from the API. That’s a bit cleaner than just switching it all off.
The final use is to disable the REST API for non-logged-in users, except for users who connect from specific IP addresses. To do this, instead of calling one of our new functions, we just set up a constant that holds an array of valid addresses, like this:
/** * Completely disable the REST API for non-logged-in users, * unless connecting from a valid IP address. */ const HW_REST_API_IP_WHITELIST = array('127.0.0.1', '::1');
You probably want to include ‘127.0.0.1’ and ‘::1’ in your IP address list, so the server can connect to itself. Just append your other whitelisted IP addresses to the end of the array.
Wrapping Up
After all that, in most cases you’ll just want to do this:
- Create the functions-rest-api.php file with the code from above.
- Include this file from your custom child theme’s functions.php file using require_once.
- Call hw_disable_rest_api_user_enumeration() from somewhere in your functions.php file.
That’s it. Time for a REST 🙂
What does the “HW” prepended to all the function names (hw_*) stand for?
I run a small WordPress hosting company called Headwall Hosting, so I sometimes prepend my functions with “hw_” for “headwall”. It’s just to a way to keep my function names unique… so they don’t clash with the functions from a plugin/theme.
Nice, thanks for the tutorials! I’m learning some good things from this site. Very useful!
Oh… Maybe just “HeadWall” for marking the code with the name of the tutorial site it came from :).
😀👍
…and adding appropriate namespacing to the defined names so they are less likely to collide with other predefined names.
Bot brute force login attempt traffic went down noticeably after calling `hw_disable_rest_api_user_enumeration()`.
Another great code snippet, thanks!
It’s amazing how much traffic this snippet blocks. I’m glad it’s useful for you 👍