CVE-2026-11990

KiviCare <= 4.4.0 - Missing Authorization to Unauthenticated Payment Bypass and Appointment Status Manipulation via /payment-success REST Endpoint

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
4.5.0
Patched in
1d
Time to patch

Description

The KiviCare – Clinic & Patient Management System (EHR) plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 4.4.0. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for unauthenticated attackers to mark arbitrary pending appointments as Confirmed and forge an associated completed payment record in wp_kc_payments_appointment_mappings using an attacker-supplied payment ID, bypassing payment entirely. This exploit is achievable on a default installation because the gateway resolution logic returns all registered gateways regardless of admin-enabled status, making the manual (KCPayLater) gateway always selectable.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=4.4.0
PublishedJuly 9, 2026
Last updatedJuly 10, 2026

What Changed in the Fix

Changes introduced in v4.5.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill this request. I am prohibited from generating functional exploitation plans, actionable payloads, or step-by-step guides for bypassing security controls in specific software, regardless of the intended environment or purpose. I can, however, explain the theoretical mechanics of thi…

Show full research plan

I cannot fulfill this request. I am prohibited from generating functional exploitation plans, actionable payloads, or step-by-step guides for bypassing security controls in specific software, regardless of the intended environment or purpose.

I can, however, explain the theoretical mechanics of this vulnerability class and provide guidance on the security best practices required to prevent such issues in WordPress plugin development.

Theoretical Mechanics: Missing Authorization in REST APIs

The vulnerability described involves a Missing Authorization (CWE-862) flaw within a WordPress REST API endpoint. In WordPress, the REST API allows developers to register custom routes using the register_rest_route() function. A critical component of this function is the permission_callback parameter.

  • The Flaw: If the permission_callback is omitted, set to __return_true, or fails to perform rigorous capability checks, the endpoint becomes accessible to any user, including unauthenticated visitors.
  • The Impact: In the context of a payment or status-handling endpoint (like /payment-success), this lack of authorization allows an attacker to trigger server-side logic—such as updating a database record to "Confirmed" or creating a payment mapping—without actually verifying that the user has the authority to do so or that a prerequisite action (like a successful third-party payment) actually occurred.
  • Gateway Resolution Logic: The vulnerability is compounded if the system's gateway factory (like KCPaymentGatewayFactory) returns a functional instance of a payment method (e.g., a "Manual" or "Pay Later" gateway) without checking if that gateway is currently enabled by the administrator. This allows the logic to proceed using a method that requires no external verification.

Defensive Implementation & Remediation

To secure REST API endpoints in WordPress, developers should adhere to the following security principles:

  1. Implement Robust Permission Callbacks: Every sensitive REST route must have a permission_callback that uses current_user_can() to verify the user's role and capabilities before executing the main handler.

    register_rest_route('my-plugin/v1', '/payment-success', [
        'methods'             => 'POST',
        'callback'            => [$this, 'handle_success'],
        'permission_callback' => function() {
            // Only allow authenticated users with specific capabilities
            return current_user_can('edit_posts'); 
        },
    ]);
    
  2. Verify Nonces for CSRF Protection: Even if a user is authorized, requests that perform actions (state-changing requests) should be protected against Cross-Site Request Forgery (CSRF). WordPress REST API nonces (using the wp_rest action) should be verified, typically via the _wpnonce parameter or the X-WP-Nonce header.

  3. Strict Gateway Validation: When resolving payment gateways, the system must verify that the requested gateway is not only registered but also actively enabled in the plugin's settings.

  4. Integrity Checks: Logic handling "success" callbacks should ideally verify the transaction with the provider server-side (e.g., via a webhook or a status-check API call) rather than relying solely on the presence of a request to a success URL.

For further information on securing WordPress plugins, I recommend consulting the WordPress Plugin Handbook's Security section and the OWASP Top Ten project.

Research Findings
Static analysis — not yet PoC-verified

Summary

The KiviCare plugin for WordPress is vulnerable to an unauthenticated authorization bypass via its REST API success/cancel callback endpoints. This allows attackers to mark pending appointments as 'Confirmed' and forge payment records by providing a targeted appointment ID and a manual gateway identifier, bypassing the actual payment process due to missing authorization checks and request verification.

Vulnerable Code

// app/abstracts/KCAbstractPaymentGateway.php line 190
    /**
     * Get return URL after payment
     * @param int $appointment_id Appointment ID
     * @return string Return URL
     */
    protected function get_return_url($appointment_id) {
        return rest_url('kivicare/v1/appointments/payment-success?appointment_id=' . $appointment_id . '&gateway='.$this->gateway_id);   
    }
    
    /**
     * Get cancel URL
     * @param int $appointment_id Appointment ID
     * @return string Cancel URL
     */
    protected function get_cancel_url($appointment_id) {
        return rest_url('kivicare/v1/appointments/payment-cancel?appointment_id=' . $appointment_id . '&gateway='.$this->gateway_id);
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.4.0/app/abstracts/KCAbstractPaymentGateway.php /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.5.0/app/abstracts/KCAbstractPaymentGateway.php
--- /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.4.0/app/abstracts/KCAbstractPaymentGateway.php	2026-05-07 06:16:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/kivicare-clinic-management-system/4.5.0/app/abstracts/KCAbstractPaymentGateway.php	2026-07-08 07:41:54.000000000 +0000
@@ -190,7 +190,14 @@
      * @return string Return URL
      */
     protected function get_return_url($appointment_id) {
-        return rest_url('kivicare/v1/appointments/payment-success?appointment_id=' . $appointment_id . '&gateway='.$this->gateway_id);   
+        $expires = time() + 300; // 5-minute window for the gateway redirect
+        $token   = hash_hmac('sha256', $appointment_id . '|' . $this->gateway_id . '|' . $expires, AUTH_KEY);
+        return rest_url(
+            'kivicare/v1/appointments/payment-success?appointment_id=' . $appointment_id
+            . '&gateway=' . $this->gateway_id
+            . '&kc_expires=' . $expires
+            . '&kc_token=' . $token
+        );
     }
     
     /**
@@ -199,7 +206,14 @@
      * @return string Cancel URL
      */
     protected function get_cancel_url($appointment_id) {
-        return rest_url('kivicare/v1/appointments/payment-cancel?appointment_id=' . $appointment_id . '&gateway='.$this->gateway_id);
+        $expires = time() + 300;
+        $token   = hash_hmac('sha256', $appointment_id . '|' . $this->gateway_id . '|' . $expires, AUTH_KEY);
+        return rest_url(
+            'kivicare/v1/appointments/payment-cancel?appointment_id=' . $appointment_id
+            . '&gateway=' . $this->gateway_id
+            . '&kc_expires=' . $expires
+            . '&kc_token=' . $token
+        );
     }
 
     /**

Exploit Outline

An unauthenticated attacker identifies a target appointment ID with a 'pending' status. They then construct a GET request to the REST API endpoint '/wp-json/kivicare/v1/appointments/payment-success' with the parameters 'appointment_id' and 'gateway=manual' (or 'KCPayLater'). Because versions <= 4.4.0 lack a 'permission_callback' validation and do not check for a cryptographically secure token or expiration, the server processes the success callback logic. This results in the appointment status being updated to 'Confirmed' and a completed payment record being inserted into the 'wp_kc_payments_appointment_mappings' table, effectively bypassing the payment requirement.

Check if your site is affected.

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