CVE-2026-3658 WordPress Simply Schedule Appointments Plugin <= 1.6.10.0 is vulnerable to a high priority SQL Injection

Overview
- Published: 2026-03-19
- CVE-ID: CVE-2026-3658
- CVSS: 7.5 High
- Affected Plugin: Simply Schedule Appointments
- Affected Versions: <= 1.6.10.0
- CWE: CWE-89 Improper Neutralization of Special Elements used in an SQL Command
Description
The Appointment Booking Calendar — Simply Schedule Appointments Booking Plugin plugin for WordPress is vulnerable to SQL Injection via the ‘fields’ parameter in all versions up to, and including, 1.6.10.0 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, including usernames, email addresses, and password hashes.
Patch And Commit Analysis
Based on the changelog details, I will provide a comparison between version 1.6.10.0 (vulnerable) and version 1.6.10.2 (patched).

This is the live patch of CVE-2026-3658, located at the entrypoint of the attack flow:
// Vulnerable Code
public function get_items( $request ) {
$params = $request->get_params();
if ( is_user_logged_in() && ! current_user_can('ssa_manage_others_appointments') ) {
// ...
}
$schema = $this->get_schema();
// Check if format=ics is defined.
$is_ics = false;
if ( isset($params['format']) && 'ics' === $params['format'] ) {
$is_ics = true;
unset( $params['format'] );
$params['fields'] = array( 'id' ); // ← chỉ hardcode khi format=ics
}
$data = $this->query( $params ); // ← nếu KHÔNG phải ics, fields từ $request đi thẳng vào
// Patch Code
public function get_items( $request ) {
$params = $request->get_params();
if ( is_user_logged_in() && ! current_user_can('ssa_manage_others_appointments') ) {
// ...
}
$schema = $this->get_schema();
// Check if format=ics is defined.
$is_ics = false;
if ( isset($params['format']) && 'ics' === $params['format'] ) {
$is_ics = true;
unset( $params['format'] );
}
// ← KHÔNG còn truyền $params['fields'] từ request xuống query()
$data = $this->query( $params );
The most important thing: The line $params['fields'] = array('id'); removed from the ics block, and more importantly, there is no longer any path that would allow $request['fields'] by attacker control to be passed into $this->query(). The plugin no longer exposes the fields parameter to the REST API.
In addition to the entrypoint fix, v1.6.10.2 also adds a whitelist at the sink level in class-td-db-model.php as defense in depth:
// Patched sink — v1.6.10.2
$valid_fields = array_keys( $this->get_fields() );
$requested = array_map( 'sanitize_key', (array) $args['fields'] );
$safe_fields = array_intersect( $requested, $valid_fields );
$fields = empty( $safe_fields ) ? '*' : '`' . implode( '`, `', $safe_fields ) . '`';
Analysis of the fields Parameter
The fields parameter plays a critical role in this vulnerability as it directly influences the structure of the SQL query rather than just supplying data values. Specifically, fields is intended to define which columns are selected in the SQL statement, meaning it is used as part of the SQL identifier (column list) in the SELECT clause.
In the vulnerable implementation, the application accepts user-supplied input for the fields parameter via the REST API and passes it through multiple layers without any form of validation, sanitization, or whitelisting. Eventually, this parameter is concatenated directly into the SQL query:
SELECT $fields FROM table_name ...
This introduces a critical security flaw because SQL identifiers (such as column names) cannot be safely parameterized using prepared statements like $wpdb->prepare(). As a result, even though the developer attempts to use a prepared statement for other parts of the query, the $fields variable remains unprotected and fully controllable by an attacker.
This allows an attacker to inject arbitrary SQL expressions into the SELECT clause, effectively transforming the query logic. For example, instead of selecting legitimate columns, an attacker can inject subqueries or database functions to extract sensitive information such as user credentials.
This misuse of dynamic SQL construction is a classic example of CWE-89 Improper Neutralization of Special Elements used in an SQL Command, but with a more subtle twist: the injection occurs in the query structure (identifier context) rather than in value context. This makes the vulnerability more dangerous and harder to mitigate if developers rely solely on prepared statements without enforcing strict input validation or whitelisting.
Root Cause Analysis
The vulnerability has 2 layers, located through 2 files. Here’s how the tainted data flows from the HTTP request into $wpdb->get_results():
- Layer 1 — Entrypoint (class-td-api-model.php (L140)): REST API endpoint receives fields from $_REQUEST or JSON body, not validated, transmitted downstream as an array.
public function get_items( $request ) {
$params = $request->get_params();
$schema = $this->get_schema();
$data = $this->query( $params );
$data = $this->prepare_collection_for_api_response( $data );
$response = array(
'response_code' => 200,
'error' => '',
'data' => $data,
);
return new WP_REST_Response( $response, 200 );
}
- Layer 2 — Sink (class-td-db-model.php L1171): Build SELECT {fields} FROM {table} with pure string concatenation, call $wpdb->get_results() with poisoned query — do not use prepared statement for SELECT clause :
$fields = empty( $args['fields'] )
? '*'
: '`' . implode( '`, `', $args['fields'] ) . '`'; // raw concat, no sanitize
$sql = $wpdb->prepare( "SELECT $fields FROM $table_name ..." );
$rows = $wpdb->get_results( $sql ); // poisoned query executed
}
Attack Flow

Step-by-Step Data Flow Analysis
- Entrypoint — class-td-api-model.php (L140) :
The plugin registers a public REST API route that does not require authentication:
public function get_items( $request ) {
$params = $request->get_params();
$schema = $this->get_schema();
$data = $this->query( $params );
$data = $this->prepare_collection_for_api_response( $data );
$response = array(
'response_code' => 200,
'error' => '',
'data' => $data,
);
return new WP_REST_Response( $response, 200 );
}
Within the get_items() function, the fields parameter is retrieved directly from the request and passed downstream without any sanitization or validation:
$fields = $request->get_param('fields');
// Example:
// ?fields[]=id&fields[]=name,SLEEP(5)--
// Parsed by PHP as: ['id', 'name,SLEEP(5)--']
The core issue at this stage is that get_param() is essentially a wrapper around user-controlled input ($_REQUEST) and does not enforce type validation, schema validation, or input sanitization. As a result, arbitrary arrays supplied by the attacker are accepted without restriction.
- Sink — class-td-db-model.php (L1171)
At the final stage, the tainted fields parameter reaches the database layer, where it is used to construct the SQL query:
$fields = empty( $args['fields'] )
? '*'
: '`' . implode( '`, `', $args['fields'] ) . '`';
// ['id', "name`, SLEEP(3), `name"] → `id`, `name`, SLEEP(3), `name`
$sql = $wpdb->prepare(
"SELECT $fields FROM $table_name $where ...",
absint( $args['offset'] ),
absint( $args['number'] )
);
$rows = $wpdb->get_results( $sql );
The critical issue here is that the $fields variable is directly interpolated into the SQL query string before the call to $wpdb->prepare(). While prepare() is used to safely bind the LIMIT parameters, it does not protect SQL identifiers such as column names.
As a result, $fields remains fully attacker-controlled and is inserted into the SELECT clause without any validation, escaping, or whitelisting.
Proof Of Concept POC
Based on the API exposure at the entry point, I used Burp Suite to intercept a legitimate request and extract the required authentication headers, including the X-WP-Nonce and session cookie. These values were then reused to craft manual requests via curl.

First, I performed a time-based SQL Injection to verify the vulnerability.
By injecting a payload using SLEEP(3), the server response was delayed by approximately 3 seconds, indicating that the input was executed within the SQL query.

To further confirm, I increased the delay to 5 seconds:

The response time increased accordingly to ~5 seconds, which confirms that the application is vulnerable to SQL Injection.
Finally, I performed a UNION-based SQL Injection to extract data from the database. Using this technique, I was able to retrieve sensitive information such as:
- Database version
- Current database user

This demonstrates that the vulnerability is fully exploitable and allows attackers to extract data from the backend database.