
CVE-2026-16540 — Simply Schedule Appointments < 1.6.12.6 Unauthenticated Appointment Data Disclosure and Mass Deletion
purge Endpoint| Field | Details |
|---|---|
| CVE | CVE-2026-16540 |
| Plugin | Simply Schedule Appointments |
| Affected Versions | < 1.6.12.6 |
| Fixed In | 1.6.12.6 |
| Active Installs | ~70,000 |
| CVSS 3.1 | 6.5 (Medium) — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N |
| CWE | CWE-863 (Incorrect Authorization) |
| OWASP | A5: Broken Access Control |
| WPVDB | c3829294-c388-4151-9e25-a3eac7b1f1c6 |
| Researcher | Suleyman Huseynov (@huseyn0vs__) |
| Disclosed | 2026-07-22 |
The GET /wp-json/ssa/v1/appointments/purge REST endpoint in Simply Schedule Appointments applies an authorization check (get_items_permissions_check) that validates an id_token proving ownership of a single appointment. However, the handler (purge_appointments()) completely ignores which appointment the token belongs to and instead operates on all appointments site-wide matching the supplied conditions.
An unauthenticated attacker who books any single appointment via the public booking form receives an id_token. Using that token, the attacker can:
The deletion cascades across 6 dependent database tables in committed transactions, with no native undo functionality.
File: includes/class-appointment-model.php
The permission gate and the handler operate on completely different scopes:
// Route registration
register_rest_route( $namespace, '/' . $base . '/purge', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'purge_appointments' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
) );
Permission check — validates ownership of ONE appointment via id_token:
public function get_items_permissions_check( $request ) {
// ...
if ( true === $this->id_token_permissions_check( $request ) ) {
return true; // grants access if caller proves ownership of one appointment
}
// ...
}
Handler — ignores the token's appointment and operates site-wide:
public function purge_appointments( WP_REST_Request $request ) {
$params = $request->get_params();
// No customer_id or appointment_id scoping — deletes ALL matching appointments
if ( isset( $params['purge_past_appointments'] ) && 'true' === $params['purge_past_appointments'] ) {
$conditions[] = $wpdb->prepare( 'end_date < %s', $date_modified_max->format( 'Y-m-d' ) );
}
$sql = 'SELECT * FROM ' . $this->get_table_name()
. ' WHERE ' . implode( ' OR ', $conditions )
. ' ORDER BY id ASC LIMIT 5000';
$list = $wpdb->get_results( $sql, ARRAY_A );
// Cascade-deletes across 6 tables in committed transactions
}
The id_token is an HMAC-MD5 derived from a single appointment's id and date_created. Possessing this token proves only that the caller booked one specific appointment — it conveys no site-wide privilege whatsoever.
1. Attacker visits the booking page and books any free appointment
→ receives confirmation email containing: appointment_id + id_token
2. GET /wp-json/ssa/v1/appointments/purge
?id=<appointment_id>
&token=<id_token>
&purge_past_appointments=true
3. Permission check passes (valid token for attacker's own appointment)
4. Handler selects ALL past appointments site-wide → returns PII in response
→ permanently deletes them across 6 tables (on premium editions)
PoC will be published on 2026-08-05 following the coordinated disclosure embargo.
See poc/ directory after the embargo date.
| Scenario | Result |
|---|---|
| Data disclosure | Names, emails, phone numbers, appointment details of all customers |
| Mass deletion (premium) | All historical/future appointments permanently destroyed across 6 tables |
| No recovery path | Requires full database backup restore |
| Date | Event |
|---|---|
| 2026-07-10 | Vulnerability discovered |
| 2026-07-10 | Submitted to WPScan |
| 2026-07-22 | CVE-2026-16540 assigned |
| 2026-07-22 | Public disclosure on WPScan |
| 2026-08-05 | PoC published |