Ocultando los endpoints de la API REST de WordPress v2 de la vista pública
Me gustaría empezar a usar la API REST de WordPress v2 para consultar información de mi sitio. He notado que cuando visito una URL de endpoint directamente, puedo ver todos los datos públicamente. También he visto que muchos tutoriales mencionan el uso de servidores de prueba o locales en lugar de sitios en producción.
Mis preguntas son:
- ¿Está diseñado para ser usado en sitios en producción?
- ¿Existe algún riesgo de seguridad al permitir que cualquiera pueda ver los endpoints, como por ejemplo
/wp-json/wp/v2/users/
que muestra todos los usuarios registrados en el sitio? - ¿Es posible permitir que solo los usuarios autorizados accedan a un endpoint?
Quiero asegurarme de seguir las mejores prácticas con respecto a la seguridad, así que cualquier consejo sería útil. La documentación de la API menciona la autenticación, pero no estoy seguro de cómo evitar que se acceda directamente a la URL. ¿Cómo suelen configurar otros desarrolladores este acceso a datos para aplicaciones externas sin exponer demasiada información?
¿Está diseñado para usarse en sitios en producción?
Sí. Muchos sitios ya lo están utilizando.
¿Existe un riesgo de seguridad al permitir que cualquiera vea los endpoints, como /wp-json/wp/v2/users/ que muestra todos los usuarios registrados en el sitio?
No. Las respuestas del servidor no tienen nada que ver con la seguridad, no hay nada que puedas hacer contra una pantalla en blanco o una respuesta de solo lectura.
Sin embargo, si tus sitios permiten contraseñas débiles, hay algunos problemas. Pero es política de tu sitio, la REST API no sabe nada sobre eso.
¿Es posible permitir que solo usuarios autorizados accedan a un endpoint?
Sí. Puedes hacerlo usando permission callback.
Por ejemplo:
if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Lo siento, no puedes ver este recurso con contexto de edición.' ), array( 'status' => rest_authorization_required_code() ) );
}
¿Cómo suelen configurar otros estos datos para que sean accesibles por aplicaciones externas sin exponer demasiada información?
Esta pregunta es difícil de responder porque no sabemos qué/cuándo es demasiada información. Pero podemos seguir estrictamente las referencias de la API y las hojas de trucos de seguridad para evitar situaciones no deseadas.

Es importante destacar: "La exposición se limita a los usuarios que han creado tipos de publicaciones que están configurados para exponerse a través de la API REST". Por lo tanto, si tienes, por ejemplo, una tienda en línea donde cada cliente es un usuario, estos usuarios no se exponen a través de /wp-json/wp/v2/users/
. (Referencia https://wordpress.stackexchange.com/q/252328/41488 comentario de @JHoffmann)

Debe tenerse en cuenta que necesitas tener un nonce basado en REST wp_create_nonce('wp_rest') en el encabezado 'X-WP-Nonce', o nada de esto funcionará y siempre devolverá un error 403.

Exactamente. Acabo de recibir un informe de Open Bug Bounty sobre esto en mi sitio WP en la versión 5.x. Incluso los script kiddies (openbugbounty.org/researchers/Cyber_World) malinterpretan el CVE para esto (cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5487). Esto fue corregido para quienes estén preocupados. No copies y pegues todo esto sin entender qué es realmente lo que lo está utilizando y si lo necesitas.

¿Es posible permitir que solo usuarios autorizados accedan a un endpoint?
Sí es posible agregar un callback de permisos personalizado a tu endpoint de API que requiera autenticación para ver el contenido. Los usuarios no autorizados recibirán una respuesta de error "code": "rest_forbidden"
La forma más sencilla de hacer esto es extendiendo el WP_REST_Posts_Controller. Aquí tienes un ejemplo muy simple de cómo hacerlo:
class My_Private_Posts_Controller extends WP_REST_Posts_Controller {
/**
* El namespace.
*
* @var string
*/
protected $namespace;
/**
* El tipo de post para el objeto actual.
*
* @var string
*/
protected $post_type;
/**
* Base REST para el objeto actual.
*
* @var string
*/
protected $rest_base;
/**
* Registrar las rutas para los objetos del controlador.
* Casi igual que WP_REST_Posts_Controller::register_routes(), pero con un
* callback de permisos personalizado.
*/
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' => __( 'Si se debe omitir la papelera y forzar la eliminación.' ),
),
),
'show_in_index' => false,
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Verificar si una solicitud dada tiene acceso para obtener items
*
* @param WP_REST_Request $request Datos completos sobre la solicitud.
* @return WP_Error|bool
*/
public function get_items_permissions_check( $request ) {
return current_user_can( 'edit_posts' );
}
}
Notarás que el callback de permisos function get_items_permissions_check
usa current_user_can
para determinar si permite el acceso. Dependiendo de cómo estés usando la API, quizás necesites aprender más sobre autenticación de clientes.
Luego puedes registrar tu tipo de post personalizado con soporte para REST API agregando los siguientes argumentos en register_post_type
/**
* Registrar un tipo de post para libros, con soporte para REST API
*
* Basado en el ejemplo de: http://codex.wordpress.org/Function_Reference/register_post_type
*/
add_action( 'init', 'my_book_cpt' );
function my_book_cpt() {
$labels = array(
'name' => _x( 'Libros', 'nombre general del tipo de post', 'your-plugin-textdomain' ),
'singular_name' => _x( 'Libro', 'nombre singular del tipo de post', 'your-plugin-textdomain' ),
'menu_name' => _x( 'Libros', 'menú admin', 'your-plugin-textdomain' ),
'name_admin_bar' => _x( 'Libro', 'añadir nuevo en la barra admin', 'your-plugin-textdomain' ),
'add_new' => _x( 'Añadir Nuevo', 'libro', 'your-plugin-textdomain' ),
'add_new_item' => __( 'Añadir Nuevo Libro', 'your-plugin-textdomain' ),
'new_item' => __( 'Nuevo Libro', 'your-plugin-textdomain' ),
'edit_item' => __( 'Editar Libro', 'your-plugin-textdomain' ),
'view_item' => __( 'Ver Libro', 'your-plugin-textdomain' ),
'all_items' => __( 'Todos los Libros', 'your-plugin-textdomain' ),
'search_items' => __( 'Buscar Libros', 'your-plugin-textdomain' ),
'parent_item_colon' => __( 'Libros Padre:', 'your-plugin-textdomain' ),
'not_found' => __( 'No se encontraron libros.', 'your-plugin-textdomain' ),
'not_found_in_trash' => __( 'No se encontraron libros en la Papelera.', 'your-plugin-textdomain' )
);
$args = array(
'labels' => $labels,
'description' => __( 'Descripción.', 'your-plugin-textdomain' ),
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'libro' ),
'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 );
}
Verás que rest_controller_class
usa My_Private_Posts_Controller
en lugar del controlador por defecto.
He tenido dificultades para encontrar buenos ejemplos y explicaciones sobre el uso de REST API fuera de la documentación. Sin embargo, encontré esta excelente explicación sobre cómo extender el controlador por defecto, y aquí hay una guía muy completa sobre cómo agregar endpoints.

Esto es lo que he utilizado para bloquear a todos los usuarios no logueados de usar la API REST por completo:
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('Lo siento, no tienes permiso para acceder a estos datos','¿haciendo trampa?',403);
}
}

A medida que el uso del punto final REST se expanda, este tipo de estrategia se volverá problemática. Al final, el punto final wp-json reemplazará al de admin-ajax, lo que significa que también habrá todo tipo de solicitudes legítimas desde el frontend. De todos modos, es mejor terminar con un 403 que con algo que podría interpretarse como contenido.

@MarkKaplun - sí, tienes razón sobre eso. Estoy usando esto en el contexto de un sitio que básicamente no ofrece datos públicos y los datos que almacenamos, incluyendo usuarios, metadatos de usuarios, datos de tipos de posts personalizados, etc., son datos propietarios que nunca deberían ser accesibles por el público. Es frustrante cuando haces mucho trabajo dentro de la estructura clásica de plantillas de WP para asegurarte de que ciertos datos sean privados y luego te das cuenta de repente de que son accesibles públicamente a través de la API REST. En fin, buen punto sobre servir un 403...

La mejor opción es desactivar el nuevo editor V5 y luego deshabilitar la API json, como se explica aquí.

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('Lo sentimos, no tienes permiso para acceder a estos datos','Autenticación Requerida',403);
} }
function json_authenticate_handler( $user ) {
global $wp_json_basic_auth_error;
$wp_json_basic_auth_error = null;
// No autenticar dos veces
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 );

¿Podrías elaborar en texto por qué y cómo esto responde a las preguntas del OP?
