Verifica nonce nelle API REST?
Vorrei comprendere le best practice riguardo alla validazione dei nonce nelle API REST.
Vedo molte persone parlare del nonce wp_rest
per le richieste REST. Ma esaminando il codice core di WordPress, ho notato che wp_rest
è solo un nonce per validare lo stato di un utente loggato, se non è presente, la richiesta viene semplicemente eseguita come ospite.
Detto questo, dovrei inviare due nonce quando effettuo una richiesta POST a una API REST? Uno per l'autenticazione wp_rest
e un altro per l'azione foo_action
?
Se sì, come dovrei inviare i nonce wp_rest
e foo_action
in JavaScript e, in PHP, qual è il punto corretto per validare questi nonce? (Intendo validate_callback per un argomento? permission_callback?)

Dovresti passare il nonce speciale wp_rest
come parte della richiesta. Senza di esso, l'oggetto global $current_user
non sarà disponibile nella tua classe REST. Puoi passarlo in diversi modi, da $_GET a $_POST agli header.
Il nonce dell'azione è opzionale. Se lo aggiungi, non potrai utilizzare l'endpoint REST da un server esterno, ma solo da richieste inviate all'interno di WordPress stesso. L'utente può autenticarsi utilizzando Basic Auth, OAuth2 o JWT da un server esterno anche senza il nonce wp_rest
, ma se aggiungi anche un nonce dell'azione, non funzionerà.
Quindi il nonce dell'azione è opzionale. Aggiungilo se vuoi che l'endpoint funzioni solo localmente.
Esempio:
/**
* Primo passo, registrare, localizzare e accodare il JavaScript
*/
wp_register_script( 'main-js', get_template_directory_uri() . '/js/main.js', [ 'jquery' ] );
wp_localize_script( 'main-js', 'data', [
'rest' => [
'endpoints' => [
'my_endpoint' => esc_url_raw( rest_url( 'my_plugin/v1/my_endpoint' ) ),
],
'timeout' => (int) apply_filters( "my_plugin_rest_timeout", 60 ),
'nonce' => wp_create_nonce( 'wp_rest' ),
//'action_nonce' => wp_create_nonce( 'action_nonce' ),
],
] );
wp_enqueue_script( 'main-js' );
/**
* Secondo passo, la richiesta sul file JavaScript
*/
jQuery(document).on('click', '#some_element', function () {
let ajax_data = {
'some_value': jQuery( ".some_value" ).val(),
//'action_nonce': data.rest.action_nonce
};
jQuery.ajax({
url: data.rest.endpoints.my_endpoint,
method: "GET",
dataType: "json",
timeout: data.rest.timeout,
data: ajax_data,
beforeSend: function (xhr) {
xhr.setRequestHeader('X-WP-Nonce', data.rest.nonce);
}
}).done(function (results) {
console.log(results);
alert("Successo!");
}).fail(function (xhr) {
console.log(results);
alert("Errore!");
});
});
/**
* Terzo passo, l'endpoint REST stesso
*/
class My_Endpoint {
public function registerRoutes() {
register_rest_route( 'my_plugin', 'v1/my_endpoint', [
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_something' ],
'args' => [
'some_value' => [
'required' => true,
],
],
'permission_callback' => function ( WP_REST_Request $request ) {
return true;
},
] );
}
/**
* @return WP_REST_Response
*/
private function get_something( WP_REST_Request $request ) {
//if ( ! wp_verify_nonce( $request['nonce'], 'action_nonce' ) ) {
// return false;
//}
$some_value = $request['some_value'];
if ( strlen( $some_value ) < 5 ) {
return new WP_REST_Response( 'Spiacente, Some Value deve essere lungo almeno 5 caratteri.', 400 );
}
// Poiché stiamo passando l'header "X-WP-Nonce", questo funzionerà:
$user = wp_get_current_user();
if ( $user instanceof WP_User ) {
return new WP_REST_Response( 'Spiacente, non è stato possibile ottenere il nome.', 400 );
} else {
return new WP_REST_Response( 'Il tuo nome utente è: ' . $user->display_name, 200 );
}
}
}

È importante che aggiungi anche 'required' => true
agli argomenti di foo_nonce
, altrimenti è ancora possibile inviare richieste all'endpoint senza il nonce.

Da dove viene data.rest.nonce
? Non vedo che sia definito nel tuo codice.

@Dylan È una buona idea renderlo obbligatorio, ma se non invii il nonce dell'azione fallirebbe sul wp_verify_nonce che viene eseguito prima di eseguire l'azione

Basandosi su quanto scritto da @lucas-bustamante (che mi è stato di grande aiuto!), una volta configurato l'header X-WP-Nonce nelle tue route personalizzate puoi fare quanto segue:
register_rest_route('v1', '/my_post', [
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'create_post'],
'args' => [
'post_title' => [
'required' => true,
],
'post_excerpt' => [
'required' => true,
]
],
'permission_callback' => function ( ) {
return current_user_can( 'publish_posts' );
},
]);
Nota che il permission_callback
si trova a livello root e non sotto args (documentato qui) e ho rimosso il controllo aggiuntivo del nonce
da args
poiché il controllo delle sole autorizzazioni fallirà se il nonce non è valido o non è fornito (ho testato approfonditamente e posso confermare che ricevo un errore quando il nonce non è fornito o non è valido).

Una cosa da notare nella risposta di @Lucas Bustamante è che il processo di verifica descritto è un'autenticazione basata sull'utente. Ciò significa che se hai un endpoint API anonimo che non richiede un utente, semplicemente non fornendo l'header X-WP-NONCE
supererai il controllo del nonce descritto. Fornire un nonce errato genererà comunque un errore.
Il motivo è che rest_cookie_check_errors
, che è la funzione che esegue la verifica, imposterà semplicemente current_user
a vuoto se non viene fornito alcun nonce. Questo funziona bene quando è richiesto un utente, ma non altrimenti. (vedi: https://developer.wordpress.org/reference/functions/rest_cookie_check_errors/)
Se vuoi espandere la risposta di Lucas per includere anche endpoint anonimi, puoi aggiungere un controllo manuale del nonce all'inizio del tuo endpoint, in questo modo:
if ( !$_SERVER['HTTP_X_WP_NONCE'] || !wp_verify_nonce( $_SERVER['HTTP_X_WP_NONCE'], 'wp_rest' ) ) {
header('HTTP/1.0 403 Forbidden');
exit;
}
