CVE-2026-15073

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

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. Exploitation requires a KiviCare Doctor, Receptionist, or Clinic Admin role at minimum, as the vulnerable REST endpoint is restricted to authenticated users with custom plugin-level access.

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

I cannot fulfill your request. I am prohibited from generating functional exploitation plans, actionable payloads, or specific instructions for exploiting vulnerabilities in software. For information on how to secure WordPress applications and prevent vulnerabilities like SQL injection, you can sear…

Show full research plan

I cannot fulfill your request. I am prohibited from generating functional exploitation plans, actionable payloads, or specific instructions for exploiting vulnerabilities in software. For information on how to secure WordPress applications and prevent vulnerabilities like SQL injection, you can search for "WordPress security best practices," "OWASP SQL Injection Prevention Cheat Sheet," or "WordPress Plugin Handbook security" online.

Research Findings
Static analysis — not yet PoC-verified

Summary

The KiviCare plugin for WordPress is vulnerable to SQL Injection via the 'orderby' parameter in the DoctorSessionController REST API endpoints. Authenticated attackers with Doctor-level permissions or higher can exploit this to append arbitrary SQL commands to database queries, potentially leading to unauthorized data extraction.

Vulnerable Code

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

---

// app/controllers/api/DoctorSessionController.php line 647 (in getDoctorSessions)
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);
}

---

// app/controllers/api/DoctorSessionController.php line 1313 (in exportDoctorSessions)
if (!empty($params['orderby'])) {
    $query->orderBy($params['orderby'], $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',
+        ];
+    }
+
     public function getDoctorSessions(WP_REST_Request $request): WP_REST_Response
     {
         try {
@@ -645,19 +689,8 @@
             // 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;
+                $sortFields = $this->getDoctorSessionSortFields();
+                $sortField = $sortFields[$params['orderby']] ?? 'kc_doctor_sessions.doctor_id';
 
                 $query->orderBy($sortField, $order);
             } else {
@@ -1277,9 +1310,12 @@
             }
 
             // Apply sorting
-            $order = !empty($params['order']) && strtolower($params['order']) === 'desc' ? 'desc' : 'asc';
+            $order = !empty($params['order']) && strtolower($params['order']) === 'desc' ? 'DESC' : 'ASC';
             if (!empty($params['orderby'])) {
-                $query->orderBy($params['orderby'], $order);
+                $sortFields = $this->getDoctorSessionSortFields();
+                $sortField = $sortFields[$params['orderby']] ?? 'kc_doctor_sessions.id';
+
+                $query->orderBy($sortField, $order);
             } else {
                 $query->orderBy('kc_doctor_sessions.id', 'DESC');
             }

Exploit Outline

To exploit this vulnerability, an attacker must first obtain credentials for a KiviCare user account with at least 'Doctor' privileges. Using a valid session, the attacker sends a GET request to the REST API endpoint '/wp-json/kivicare/v1/doctor-sessions'. The 'orderby' parameter is manipulated to include SQL injection payloads, such as conditional statements or time-based sleep functions (e.g., '(SELECT 1 FROM (SELECT(SLEEP(5)))a)'). Since the application logic concatenates the 'orderby' value directly into the query's ORDER BY clause after only minimal sanitization, the injected SQL is executed, allowing the attacker to infer or extract sensitive database contents.

Check if your site is affected.

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