KiviCare <= 4.5.0 - Authenticated (Doctor+) SQL Injection via 'orderby' Parameter in DoctorSessionController
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:NTechnical Details
<=4.5.0What Changed in the Fix
Changes introduced in v4.5.1
Source Code
WordPress.org SVNI 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.
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
@@ -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.