Wordpress - WordPress 4.7.1 REST API still exposing users

Use this code snippet it will hide the users list and give 404 as the result, while rest of the api calls keep running as they were.

add_filter( 'rest_endpoints', function( $endpoints ){
    if ( isset( $endpoints['/wp/v2/users'] ) ) {
        unset( $endpoints['/wp/v2/users'] );
    }
    if ( isset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] ) ) {
        unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
    }
    return $endpoints;
});

You can refer to the this link on gitHub repo of WP_REST_API for some more details on same.

::UPDATE::

To remove all default REST API end-points you have to add following code:

<?php remove_action('rest_api_init', 'create_initial_rest_routes', 99); ?>


Remove the API link from the HTML head if you like.

// https://wordpress.stackexchange.com/a/211469/77054
// https://wordpress.stackexchange.com/a/212472
remove_action( 'wp_head', 'rest_output_link_wp_head', 10 );

Then require authentication for all requests.

// You can require authentication for all REST API requests by adding an is_user_logged_in check to the rest_authentication_errors filter.
add_filter( 'rest_authentication_errors', function( $result ) {
    if ( ! empty( $result ) ) {
        return $result;
    }
    if ( ! is_user_logged_in() ) {
        return new WP_Error( 'rest_not_logged_in', 'Only authenticated users can access the REST API.', array( 'status' => 401 ) );
    }
    return $result;
});

This will leave you with the desired message.

Now to stop enumeration you could use something like this.

// https://perishablepress.com/stop-user-enumeration-wordpress/
// block WP enum scans
    // https://m0n.co/enum
    if (!is_admin()) {
        // default URL format
        if (preg_match('/author=([0-9]*)/i', $_SERVER['QUERY_STRING'])) die();
        add_filter('redirect_canonical', 'shapeSpace_check_enum', 10, 2);
    }
    function shapeSpace_check_enum($redirect, $request) {
        // permalink URL format
        if (preg_match('/\?author=([0-9]*)(\/*)/i', $request)) die();
        else return $redirect;
    }

Check out the whole post for further techniques.


/**
 * Wrap an existing default callback passed in parameter and create
 * a new permission callback introducing preliminary checks and
 * falling-back on the default callback in case of success.
 */
function permission_callback_hardener ($existing_callback) {
    return function ($request) use($existing_callback) {
        if (! current_user_can('list_users')) {
            return new WP_Error(
                'rest_user_cannot_view',
                __( 'Sorry, you are not allowed to access users.' ),
                [ 'status' => rest_authorization_required_code() ]
            );
        }

        return $existing_callback($request);
    };
}

function api_users_endpoint_force_auth($endpoints)
{
    $users_get_route = &$endpoints['/wp/v2/users'][0];
    $users_get_route['permission_callback'] = permission_callback_hardener($users_get_route['permission_callback']);

    $user_get_route = &$endpoints['/wp/v2/users/(?P<id>[\d]+)'][0];
    $user_get_route['permission_callback'] = permission_callback_hardener($user_get_route['permission_callback']);

    return $endpoints;
}

add_filter('rest_endpoints', 'api_users_endpoint_force_auth');
  • The endpoint(s) is not blocked for administrators (Gutenberg keeps working)
  • The endpoint rejects anonymous users in a proper way.
  • It's generic enough to support further endpoints.
  • The current_user_can could be further enhanced, made more generic.
  • Assume that the GET method is the first for a registered route (which so far has always been true)