Nascondere gli endpoint WordPress REST API v2 dalla visualizzazione pubblica
Vorrei iniziare a utilizzare la WordPress REST API v2 per interrogare le informazioni dal mio sito. Ho notato che quando visito direttamente l'URL di un endpoint, posso vedere tutti i dati pubblicamente. Ho anche visto che molti tutorial menzionano l'uso di server di test o locali piuttosto che siti live.
Le mie domande sono:
- È pensata per essere utilizzata sui siti in produzione?
- Esiste un rischio per la sicurezza nel permettere a chiunque di visualizzare gli endpoint, come
/wp-json/wp/v2/users/
che mostra tutti gli utenti registrati al sito? - È possibile consentire l'accesso a un endpoint solo agli utenti autorizzati?
Voglio assicurarmi di seguire le migliori pratiche riguardo alla sicurezza, quindi qualsiasi suggerimento sarebbe utile. La documentazione delle API menziona l'autenticazione, ma non sono sicuro di come impedire che l'URL venga accessibile direttamente. Come fanno solitamente gli altri a configurare questi dati per essere accessibili da applicazioni esterne senza esporre troppe informazioni?
È pensato per essere utilizzato su siti in produzione?
Sì. Molti siti lo stanno già utilizzando.
C'è un rischio per la sicurezza nel consentire a chiunque di visualizzare gli endpoint, come /wp-json/wp/v2/users/ che mostra tutti gli utenti registrati sul sito?
No. Le risposte del server non hanno nulla a che fare con la sicurezza, non puoi fare nulla contro uno schermo vuoto o una risposta di sola lettura.
Tuttavia, se i tuoi siti consentono password deboli, ci sono alcuni problemi. Ma è una politica del tuo sito, l'API REST non ne sa nulla.
È possibile consentire solo agli utenti autorizzati di accedere a un endpoint?
Sì. Puoi farlo utilizzando il callback di autorizzazione.
Ad esempio:
if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
return new WP_Error( 'rest_forbidden_context', __( 'Spiacenti, non puoi visualizzare questa risorsa con contesto di modifica.' ), array( 'status' => rest_authorization_required_code() ) );
}
Come fanno di solito gli altri a impostare questi dati per essere accessibili da applicazioni esterne senza esporre troppe informazioni?
Questa domanda è difficile da rispondere perché non sappiamo cosa/quando sia troppa informazione. Ma possiamo seguire rigorosamente i riferimenti API e i cheatsheet di sicurezza per evitare situazioni indesiderate.

Importante da notare: "L'esposizione è limitata agli utenti che hanno creato tipi di post impostati per essere esposti tramite REST API." - quindi se hai, ad esempio, un negozio online dove ogni cliente ha un utente, questi utenti non sono esposti tramite /wp-json/wp/v2/users/
. (Riferimento https://wordpress.stackexchange.com/q/252328/41488 @JHoffmann comment)

È importante notare che è necessario avere un nonce basato su REST wp_create_nonce('wp_rest') nell'header 'X-WP-Nonce', altrimenti nessuna di queste cose funzionerà e restituirà sempre un 403.

Esatto. Ho appena ricevuto un Open Bug Bounty riguardo questo sul mio sito WP sulla versione 5.x. Anche gli script kiddies (openbugbounty.org/researchers/Cyber_World) fraintendono il CVE per questo (cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-5487). Questo è stato patchato per chiunque fosse preoccupato. Non copiate e incollate tutto questo senza capire cosa lo sta realmente utilizzando e se ne avete bisogno.

È possibile consentire l'accesso a un endpoint solo agli utenti autorizzati?
È possibile aggiungere un callback di autorizzazione personalizzato al tuo endpoint API che richiede l'autenticazione per visualizzare il contenuto. Gli utenti non autorizzati riceveranno una risposta di errore "code": "rest_forbidden"
Il modo più semplice per farlo è estendere il WP_REST_Posts_Controller. Ecco un esempio molto semplice:
class My_Private_Posts_Controller extends WP_REST_Posts_Controller {
/**
* Il namespace.
*
* @var string
*/
protected $namespace;
/**
* Il post type per l'oggetto corrente.
*
* @var string
*/
protected $post_type;
/**
* Base REST per l'oggetto corrente.
*
* @var string
*/
protected $rest_base;
/**
* Registra le route per gli oggetti del controller.
* Quasi lo stesso di WP_REST_Posts_Controller::register_routes(), ma con un
* callback di autorizzazione personalizzato.
*/
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' => __( 'Se bypassare il cestino e forzare l\'eliminazione.' ),
),
),
'show_in_index' => false,
),
'schema' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Verifica se una determinata richiesta ha accesso agli elementi
*
* @param WP_REST_Request $request Dati completi sulla richiesta.
* @return WP_Error|bool
*/
public function get_items_permissions_check( $request ) {
return current_user_can( 'edit_posts' );
}
}
Noterai che il callback di autorizzazione function get_items_permissions_check
utilizza current_user_can
per determinare se consentire l'accesso. A seconda di come stai utilizzando l'API, potresti aver bisogno di approfondire l'autenticazione del client.
Puoi quindi registrare il tuo custom post type con supporto REST API aggiungendo i seguenti argomenti in register_post_type
/**
* Registra un custom post type per i libri, con supporto REST API
*
* Basato sull'esempio: http://codex.wordpress.org/Function_Reference/register_post_type
*/
add_action( 'init', 'my_book_cpt' );
function my_book_cpt() {
$labels = array(
'name' => _x( 'Libri', 'nome generale del post type', 'your-plugin-textdomain' ),
'singular_name' => _x( 'Libro', 'nome singolare del post type', 'your-plugin-textdomain' ),
'menu_name' => _x( 'Libri', 'menu admin', 'your-plugin-textdomain' ),
'name_admin_bar' => _x( 'Libro', 'aggiungi nuovo nella barra admin', 'your-plugin-textdomain' ),
'add_new' => _x( 'Aggiungi nuovo', 'libro', 'your-plugin-textdomain' ),
'add_new_item' => __( 'Aggiungi nuovo libro', 'your-plugin-textdomain' ),
'new_item' => __( 'Nuovo libro', 'your-plugin-textdomain' ),
'edit_item' => __( 'Modifica libro', 'your-plugin-textdomain' ),
'view_item' => __( 'Visualizza libro', 'your-plugin-textdomain' ),
'all_items' => __( 'Tutti i libri', 'your-plugin-textdomain' ),
'search_items' => __( 'Cerca libri', 'your-plugin-textdomain' ),
'parent_item_colon' => __( 'Libri genitore:', 'your-plugin-textdomain' ),
'not_found' => __( 'Nessun libro trovato.', 'your-plugin-textdomain' ),
'not_found_in_trash' => __( 'Nessun libro trovato nel cestino.', 'your-plugin-textdomain' )
);
$args = array(
'labels' => $labels,
'description' => __( 'Descrizione.', '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 );
}
Noterai che rest_controller_class
utilizza My_Private_Posts_Controller
invece del controller predefinito.
Ho trovato difficile trovare buoni esempi e spiegazioni per l'uso della REST API al di fuori della documentazione. Ho trovato questa ottima spiegazione sull'estensione del controller predefinito, e qui c'è una guida molto approfondita per aggiungere endpoint.

Ecco quello che ho utilizzato per bloccare tutti gli utenti non loggati dall'utilizzare completamente l'API REST:
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('Spiacente, non sei autorizzato ad accedere a questi dati','cheatin eh?',403);
}
}

Man mano che l'uso degli endpoint REST si espanderà, questo tipo di strategia diventerà problematica. Alla fine l'endpoint wp-json sostituirà quello di admin-ajax, il che significa che ci saranno anche tutti i tipi di richieste legittime dal frontend. Comunque, è meglio morire con un 403 che con qualcosa che potrebbe essere interpretato come contenuto.

@MarkKaplun - sì, hai ragione su questo. Sto usando questo nel contesto di un sito che essenzialmente non offre alcun dato pubblico e i dati che stiamo memorizzando, inclusi utenti, metadati utente, dati di custom post type, ecc. sono dati proprietari che non dovrebbero mai essere accessibili al pubblico. È brutto quando fai un sacco di lavoro all'interno della struttura classica dei template di WP per assicurarti che certi dati siano privati e poi ti rendi conto all'improvviso che sono tutti accessibili pubblicamente tramite la REST API. Comunque, buon punto riguardo a servire un 403...

L'opzione migliore è disabilitare il nuovo editor V5 e poi disabilitare l'API JSON, come spiegato qui.

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('Spiacente, non sei autorizzato ad accedere a questi dati','Autenticazione richiesta',403);
} }
function json_authenticate_handler( $user ) {
global $wp_json_basic_auth_error;
$wp_json_basic_auth_error = null;
// Non autenticare due volte
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 );

Potresti elaborare nel testo perché e come questo risponde alle domande dell'OP?
