24

I would like to start using the WordPress REST API v2 to query information from my site. I've noticed that when I visit an endpoint URL directly, I can see all of the data publicly. I've also seen that a lot of tutorials mention the use of test or local servers rather than live sites.

My questions are:

  • Is this meant to be used on sites in production?
  • Is there a security risk to allowing endpoints to be viewed by anyone, such as /wp-json/wp/v2/users/ which shows all users registered to the site?
  • Is it possible to allow only authorized users to access an endpoint?

I want to make sure that I am following best practices regarding security, so any tips would be helpful. The api docs mention authentication, but I'm not sure how to prevent the URL from being accessed directly. How do others usually set up this data to be accessed by external applications without exposing too much information?

2
  • 2
    The real question is, are you using the endpoints client side (i.e. in AJAX calls), or server side (perhaps from another application)? Commented Jun 2, 2016 at 18:32
  • 1
    Note: The most recent version of the WordFence plugin has an option to "Prevent discovery of usernames through '/?author=N' scans, the oEmbed API, and the WordPress REST API" Commented Jan 11, 2018 at 3:37

5 Answers 5

22

Is this meant to be used on sites in production?

Yes. Many sites have been already using it.

Is there a security risk to allowing endpoints to be viewed by anyone, such as /wp-json/wp/v2/users/ which shows all users registered to the site?

No. Server responses have nothing to do with security, nothing you can do against a blank screen or read only response.

However, If your sites allow weak passwords, there're some problems. But it's your site's policy, REST API knows nothing about that.

Is it possible to allow only authorized users to access an endpoint?

Yes. You can do it by using permission callback.

For example:

if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
    return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you cannot view this resource with edit context.' ), array( 'status' => rest_authorization_required_code() ) );
}

How do others usually set up this data to be accessed by external applications without exposing too much information?

This question is hard to answer because we don't know what/when is too much information. But we can strictly follow API references and security cheatsheets to avoid unwanted situation.

3
  • 3
    Important to note: "Exposure gets limited to users who have authored post types which are set to be exposed via REST API." - so if you have say, an online store where every customer has a user, these users are not exposed via /wp-json/wp/v2/users/. (Reference wordpress.stackexchange.com/q/252328/41488 @JHoffmann comment) Commented Nov 10, 2017 at 16:48
  • 1
    It should be noted that you need to have a REST based nonce wp_create_nonce('wp_rest') in the 'X-WP-Nonce' header, or none of this stuff will work at all, and will always return a 403. Commented Dec 12, 2018 at 9:17
  • Exactly. I just received an Open Bug Bounty about this on my WP site on 5.x. Even the script kiddies (openbugbounty.org/researchers/Cyber_World) misunderstand the CVE for this (cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5487). This was patched for anyone concerned. Don't copy and paste all of these without understanding what really is using it and if you need it.
    – Tony-Caffe
    Commented Nov 19, 2021 at 20:00
8

Is it possible to allow only authorized users to access an endpoint?

It is possible to add a custom permission callback to your API endpoint which requires authentication to view the content. Unauthorized users will receive an error response "code": "rest_forbidden"

The simplest way to do this is to extend the WP_REST_Posts_Controller. Here's a very simple example of that:

class My_Private_Posts_Controller extends WP_REST_Posts_Controller {

   /**
   * The namespace.
   *
   * @var string
   */
   protected $namespace;

   /**
   * The post type for the current object.
   *
   * @var string
   */
   protected $post_type;

   /**
   * Rest base for the current object.
   *
   * @var string
   */
   protected $rest_base;

  /**
   * Register the routes for the objects of the controller.
   * Nearly the same as WP_REST_Posts_Controller::register_routes(), but with a 
   * custom permission callback.
   */
  public function register_routes() {
    register_rest_route( $this->namespace, '/' . $this->rest_base, array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_items' ),
            'permission_callback' => array( $this, 'get_items_permissions_check' ),
            'args'                => $this->get_collection_params(),
            'show_in_index'       => true,
        ),
        array(
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => array( $this, 'create_item' ),
            'permission_callback' => array( $this, 'create_item_permissions_check' ),
            'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
            'show_in_index'       => true,
        ),
        'schema' => array( $this, 'get_public_item_schema' ),
    ) );

    register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_item' ),
            'permission_callback' => array( $this, 'get_item_permissions_check' ),
            'args'                => array(
                'context' => $this->get_context_param( array( 'default' => 'view' ) ),
            ),
            'show_in_index'       => true,
        ),
        array(
            'methods'             => WP_REST_Server::EDITABLE,
            'callback'            => array( $this, 'update_item' ),
            'permission_callback' => array( $this, 'update_item_permissions_check' ),
            'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
            'show_in_index'       => true,
        ),
        array(
            'methods'             => WP_REST_Server::DELETABLE,
            'callback'            => array( $this, 'delete_item' ),
            'permission_callback' => array( $this, 'delete_item_permissions_check' ),
            'args'                => array(
                'force' => array(
                    'default'     => true,
                    'description' => __( 'Whether to bypass trash and force deletion.' ),
                ),
            ),
            'show_in_index'       => false,
        ),
        'schema' => array( $this, 'get_public_item_schema' ),
    ) );     
  }

  /**
   * Check if a given request has access to get items
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function get_items_permissions_check( $request ) {
    return current_user_can( 'edit_posts' );
  }

}

You'll notice that the permissions callback function get_items_permissions_check uses current_user_can to determine whether to allow access. Depending on how you're using the API, you may need to learn more about client authentication.

You can then register your custom post type with REST API support by adding the following arguments in register_post_type

  /**
   * Register a book post type, with REST API support
   *
   * Based on example at: http://codex.wordpress.org/Function_Reference/register_post_type
   */
  add_action( 'init', 'my_book_cpt' );
  function my_book_cpt() {
    $labels = array(
        'name'               => _x( 'Books', 'post type general name', 'your-plugin-textdomain' ),
        'singular_name'      => _x( 'Book', 'post type singular name', 'your-plugin-textdomain' ),
        'menu_name'          => _x( 'Books', 'admin menu', 'your-plugin-textdomain' ),
        'name_admin_bar'     => _x( 'Book', 'add new on admin bar', 'your-plugin-textdomain' ),
        'add_new'            => _x( 'Add New', 'book', 'your-plugin-textdomain' ),
        'add_new_item'       => __( 'Add New Book', 'your-plugin-textdomain' ),
        'new_item'           => __( 'New Book', 'your-plugin-textdomain' ),
        'edit_item'          => __( 'Edit Book', 'your-plugin-textdomain' ),
        'view_item'          => __( 'View Book', 'your-plugin-textdomain' ),
        'all_items'          => __( 'All Books', 'your-plugin-textdomain' ),
        'search_items'       => __( 'Search Books', 'your-plugin-textdomain' ),
        'parent_item_colon'  => __( 'Parent Books:', 'your-plugin-textdomain' ),
        'not_found'          => __( 'No books found.', 'your-plugin-textdomain' ),
        'not_found_in_trash' => __( 'No books found in Trash.', 'your-plugin-textdomain' )
    );

    $args = array(
        'labels'             => $labels,
        'description'        => __( 'Description.', 'your-plugin-textdomain' ),
        'public'             => true,
        'publicly_queryable' => true,
        'show_ui'            => true,
        'show_in_menu'       => true,
        'query_var'          => true,
        'rewrite'            => array( 'slug' => 'book' ),
        'capability_type'    => 'post',
        'has_archive'        => true,
        'hierarchical'       => false,
        'menu_position'      => null,
        'show_in_rest'       => true,
        'rest_base'          => 'books-api',
        'rest_controller_class' => 'My_Private_Posts_Controller',
        'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' )
    );

    register_post_type( 'book', $args );
}

You'll see rest_controller_class uses My_Private_Posts_Controller instead of the default controller.

I've been finding it difficult to find good examples and explanations for using the REST API outside the documentation. I did find this great explanation of extending the default controller, and here's a very thorough guide to adding endpoints.

1
  • Thank you, worked well for my needs - although don't forget to include a namespace in the controller class if you are using namespaces - as there is no error if the controller class is not found ( yes I forgot! ) Commented Jun 4, 2020 at 14:06
2

Here is what I have used to block all non-logged in users from using the REST API at all:

add_filter( 'rest_api_init', 'rest_only_for_authorized_users', 99 );
function rest_only_for_authorized_users($wp_rest_server){
    if ( !is_user_logged_in() ) {
        wp_die('sorry you are not allowed to access this data','cheatin eh?',403);
    }
}
2
  • As the use of the rest end point is going to expand, this kind of strategy will become problematic. In the end the wp-json end point will replace the admin-ajax one, meaning that there will also be all kinds of legitimate front end requests. Anyway, better to die with a 403 than something that might be interpreted as content. Commented Dec 1, 2017 at 5:22
  • 1
    @MarkKaplun - yes, you are correct about that. I'm using this in the context of a site that offers essentially no public data at all and the data that we are storing including users, user meta, custom post type data, etc is proprietary data that should not be accessed ever by the public. It sucks when you do a bunch of work within the classic WP template structure to make sure certain data is private and then realize suddenly that it's all accessible publicly via the REST API. Anyways, good point about serving a 403... Commented Dec 1, 2017 at 14:29
1

Best option is to disable V5 new editor and then disable API json, as explains here.

https://codber.com/2020/05/01/how-to-disable-wordpress-rest-api-to-not-logged-in-user-without-a-plugin/

1
  • Link only answer will become obsolete with expired links. You can edit your answer with the gist of the answer and then can link for more details. :) Commented May 10, 2020 at 18:56
0
add_filter( 'rest_api_init', 'rest_only_for_authorized_users', 99 );
function rest_only_for_authorized_users($wp_rest_server)
{
if( !is_user_logged_in() ) 

    wp_die('sorry you are not allowed to access this data','Require Authentication',403);
} } 
function json_authenticate_handler( $user ) {

global $wp_json_basic_auth_error;

$wp_json_basic_auth_error = null;

// Don't authenticate twice
if ( ! empty( $user ) ) {
    return $user;
}

if ( !isset( $_SERVER['PHP_AUTH_USER'] ) ) {
    return $user;
}

$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];


remove_filter( 'determine_current_user', 'json_authenticate_handler', 20 );

$user = wp_authenticate( $username, $password );

add_filter( 'determine_current_user', 'json_authenticate_handler', 20 );

if ( is_wp_error( $user ) ) {
    $wp_json_basic_auth_error = $user;
    return null;
}

$wp_json_basic_auth_error = true;

return $user->ID;}add_filter( 'determine_current_user', 'json_authenticate_handler', 20 );
2
  • 1
    Could you elaborate in text why and how this answers the OP's questions?
    – kero
    Commented Jan 8, 2018 at 12:32
  • This is not op's answer and I gave only code to show how to work in practically and I have tried to you to get easily understood in programmatically If you understood it Commented Jan 9, 2018 at 13:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.