CVE-2026-2324

LatePoint – Calendar Booking Plugin for Appointments and Events <= 5.2.7 - Cross-Site Request Forgery in Booking Form Settings Update to Stored Cross-Site Scripting

mediumCross-Site Request Forgery (CSRF)
6.1
CVSS Score
6.1
CVSS Score
medium
Severity
5.2.8
Patched in
1d
Time to patch

Description

The LatePoint – Calendar Booking Plugin for Appointments and Events plugin for WordPress is vulnerable to Cross-Site Request Forgery in all versions up to, and including, 5.2.7. This is due to missing or incorrect nonce validation on the reload_preview() function. This makes it possible for unauthenticated attackers to update settings and inject malicious web scripts via a forged request granted they can trick a site administrator into performing an action such as clicking on a link.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=5.2.7
PublishedMarch 10, 2026
Last updatedMarch 11, 2026
Affected pluginlatepoint

What Changed in the Fix

Changes introduced in v5.2.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-2324 LatePoint CSRF to Stored XSS ## 1. Vulnerability Summary The **LatePoint** plugin (up to 5.2.7) is vulnerable to **Cross-Site Request Forgery (CSRF)** in the `OsBookingFormSettingsController::reload_preview()` function. This function lacks proper nonce validation, all…

Show full research plan

Research Plan: CVE-2026-2324 LatePoint CSRF to Stored XSS

1. Vulnerability Summary

The LatePoint plugin (up to 5.2.7) is vulnerable to Cross-Site Request Forgery (CSRF) in the OsBookingFormSettingsController::reload_preview() function. This function lacks proper nonce validation, allowing an unauthenticated attacker to trick a logged-in administrator into updating plugin settings. Specifically, the attacker can inject malicious JavaScript into the booking form settings, resulting in Stored Cross-Site Scripting (XSS) that executes when the booking form is viewed on the frontend or in the admin preview.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: latepoint_route_call
  • Route Name: booking_form_settings__reload_preview
  • Vulnerable Parameter: steps_settings[contact][main_panel_content_before] (and other main_panel_content fields).
  • Authentication Required: Administrator (via CSRF).
  • Preconditions: An administrator must be logged in and tricked into visiting a malicious webpage or clicking a link.

3. Code Flow

  1. Entry Point: The request hits admin-ajax.php with action=latepoint_route_call.
  2. Routing: The LatePoint router (likely in OsRouterHelper) dispatches the request to OsBookingFormSettingsController::reload_preview().
  3. Missing Security: Unlike other methods (e.g., OsCustomersController::destroy()), reload_preview() does not call $this->check_nonce().
  4. Processing Loop:
    • The function iterates through steps_settings.
    • For specific keys like side_panel_heading or side_panel_description, it applies wp_strip_all_tags or strip_tags.
    • Crucially, keys like main_panel_content_before and main_panel_content_after are not sanitized:
      // lib/controllers/booking_form_settings_controller.php
      foreach ( $steps as $step_code ) {
          if ( ! empty( $this->params['steps_settings'][ $step_code ] ) ) {
              foreach ( $this->params['steps_settings'][ $step_code ] as $step_setting_key => $step_setting_value ) {
                  // ... only specific keys are sanitized ...
                  $steps_settings[ $step_code ][ $step_setting_key ] = trim( $step_setting_value );
              }
          }
      }
      
  5. Sink: The unsanitized settings are saved to the database via OsStepsHelper::save_steps_settings( $steps_settings );.
  6. Reflection: In lib/views/booking_form_settings/_booking_form_preview.php, the values are echoed directly:
    <div class="bf-main-panel-content-before ...">
        <?php echo OsStepsHelper::get_step_setting_value($selected_step_code, 'main_panel_content_before'); ?>
    </div>
    

4. Nonce Acquisition Strategy

Although the vulnerability is listed as missing/incorrect nonce validation, the LatePoint framework often requires a latepoint_nonce for its generic latepoint_route_call action to work at all for admins.

  1. Identify Script Context: LatePoint settings are managed in the admin dashboard under "Booking Form Settings".
  2. Navigation: Navigate to wp-admin/admin.php?page=latepoint&route_name=booking_form_settings__preview.
  3. Extraction: Use browser_eval to extract the admin nonce from the latepoint_helper global object (common in LatePoint).
    • Target Variable: window.latepoint_helper?.nonce or window.latepoint_admin_params?.nonce.
  4. Bypass Check: If the endpoint validates the nonce but uses a generic action string (like latepoint_nonce), we must ensure we use that specific nonce.

5. Exploitation Strategy

The exploit will be a CSRF that triggers the reload_preview action to save a malicious payload.

  • Step 1: Admin Nonce Extraction
    Use browser_navigate to the LatePoint dashboard, then browser_eval to grab the nonce.

  • Step 2: CSRF POST Request
    Use the http_request tool to simulate the admin's browser making the request.

    • Method: POST
    • URL: http://vulnerable-site.com/wp-admin/admin-ajax.php
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Payload:
      action=latepoint_route_call
      &route_name=booking_form_settings__reload_preview
      &latepoint_nonce=[EXTRACTED_NONCE]
      &selected_step_code=contact
      &steps_settings[contact][main_panel_content_before]=<script>alert('CVE-2026-2324_XSS')</script>
      
  • Step 3: Verification of Storage
    Navigate to a page containing the LatePoint booking form (or the preview page) and check for the injected script execution.

6. Test Data Setup

  1. Enable Plugin: Ensure latepoint is active.
  2. Create Frontend Page: Create a WordPress page with the LatePoint booking shortcode:
    • wp post create --post_type=page --post_status=publish --post_title="Booking" --post_content='[latepoint_book_button]'
  3. Identify Step: The "Contact" step (contact) is standard in most LatePoint configurations.

7. Expected Results

  • The admin-ajax.php request should return a JSON response with "status": "success" and include booking_form_html containing the injected script.
  • Upon visiting the "Booking" page created in setup, a browser alert CVE-2026-2324_XSS should fire.

8. Verification Steps (Post-Exploit)

  • Database Check: Use WP-CLI to verify the stored setting:
    wp option get latepoint_steps_settings --format=json
    Check if the contact -> main_panel_content_before key contains the <script> tag.
  • Preview Verification: Navigate to the admin preview:
    wp-admin/admin.php?page=latepoint&route_name=booking_form_settings__preview
    The script should execute here as well.

9. Alternative Approaches

  • Shared Settings Sink: The reload_preview function also processes params['steps_settings']['shared'] and calls OsSettingsHelper::save_setting_by_name. If main_panel_content_before is filtered, try injecting via steps_support_text which allows several HTML tags (though script is stripped via strip_tags).
  • General Settings Sink: Use params['settings'] to overwrite other plugin settings if steps_settings are sanitized in a specific environment. The loop over params['settings'] calls OsSettingsHelper::prepare_value which might be less restrictive for custom setting names.
Research Findings
Static analysis — not yet PoC-verified

Summary

The LatePoint plugin for WordPress is vulnerable to Cross-Site Request Forgery (CSRF) in its booking form settings update functionality. Unauthenticated attackers can trick an administrator into submitting a crafted request that updates plugin settings with malicious JavaScript, leading to Stored Cross-Site Scripting (XSS).

Vulnerable Code

// lib/controllers/booking_form_settings_controller.php

public function reload_preview() {
    OsStepsHelper::set_cart_object();
    OsStepsHelper::set_booking_object();
    OsStepsHelper::set_restrictions();
    OsStepsHelper::set_presets();
    OsStepsHelper::set_active_cart_item_object();
    $steps_settings = OsStepsHelper::get_steps_settings();
    $steps          = OsStepsHelper::get_step_codes_in_order();
    foreach ( $steps as $step_code ) {
        if ( ! empty( $this->params['steps_settings'][ $step_code ] ) ) {
            foreach ( $this->params['steps_settings'][ $step_code ] as $step_setting_key => $step_setting_value ) {
                // remove empty content for text content settings
                if ( in_array( $step_setting_value, [ '<p><br></p>', '<br>', '<br/>' ] ) ) {
                    $step_setting_value = '';
                }
                if(in_array($step_setting_key, ['side_panel_heading', 'main_panel_heading'])) $step_setting_value = wp_strip_all_tags($step_setting_value);
                if(in_array($step_setting_key, ['side_panel_description'])) $step_setting_value = strip_tags($step_setting_value, ['a', 'i', 'u', 'b', 'br']);
                $steps_settings[ $step_code ][ $step_setting_key ] = trim( $step_setting_value );
            }
        }
    }
    // ... 
    OsStepsHelper::save_steps_settings( $steps_settings );

---

// lib/views/booking_form_settings/_booking_form_preview.php

<div class="bf-main-panel-content-before os-editable editable-setting" data-placeholder="+" data-setting-key="[<?php echo esc_attr($selected_step_code);?>][main_panel_content_before]"><?php echo OsStepsHelper::get_step_setting_value($selected_step_code, 'main_panel_content_before'); ?></div>
<div class="bf-main-panel-content">
    <?php echo OsStepsHelper::get_step_content_preview($selected_step_code); ?>
</div>
<div class="bf-main-panel-content-after os-editable editable-setting" data-placeholder="+" data-setting-key="[<?php echo esc_attr($selected_step_code);?>][main_panel_content_after]"><?php echo OsStepsHelper::get_step_setting_value($selected_step_code, 'main_panel_content_after'); ?></div>

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.7/lib/controllers/booking_form_settings_controller.php /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.8/lib/controllers/booking_form_settings_controller.php
--- /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.7/lib/controllers/booking_form_settings_controller.php	2025-05-11 07:47:12.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.8/lib/controllers/booking_form_settings_controller.php	2026-02-18 05:33:34.000000000 +0000
@@ -23,6 +23,9 @@
 		}
 
 		public function reload_preview() {
+			// Verify nonce.
+			$this->check_nonce( 'reload_preview' );
+
 			OsStepsHelper::set_cart_object();
 			OsStepsHelper::set_booking_object();
 			OsStepsHelper::set_restrictions();
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.7/lib/views/booking_form_settings/_booking_form_preview.php /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.8/lib/views/booking_form_settings/_booking_form_preview.php
--- /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.7/lib/views/booking_form_settings/_booking_form_preview.php	2025-05-11 07:47:12.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/latepoint/5.2.8/lib/views/booking_form_settings/_booking_form_preview.php	2026-02-18 05:33:34.000000000 +0000
@@ -20,20 +20,37 @@
                     </div>
                 </div>
 				<div class="bf-side-heading editable-setting" data-setting-key="[<?php echo esc_attr($selected_step_code);?>][side_panel_heading]" contenteditable="true"><?php echo wp_strip_all_tags(OsStepsHelper::get_step_setting_value($selected_step_code, 'side_panel_heading')); ?></div>
-				<div class="bf-side-desc os-editable-basic editable-setting" data-setting-key="[<?php echo $selected_step_code;?>][side_panel_description]"><?php echo strip_tags(OsStepsHelper::get_step_setting_value($selected_step_code, 'side_panel_description'), ['a', 'i', 'u', 'b', 'br']); ?></div>
+				<div
+					class="bf-side-desc os-editable-basic editable-setting"
+					data-setting-key="[<?php echo esc_attr( $selected_step_code ); ?>][side_panel_description]"
+				>
+					<?php echo strip_tags( OsStepsHelper::get_step_setting_value( $selected_step_code, 'side_panel_description' ), [ 'a', 'i', 'u', 'b', 'br' ] ); ?>
+				</div>
 			</div>
 			<div class="side-panel-extra os-editable editable-setting" data-setting-key="[shared][steps_support_text]">
-				<?php echo OsSettingsHelper::get_steps_support_text(); ?>
+				<?php echo wp_kses_post( OsSettingsHelper::get_steps_support_text() ); ?>
 			</div>
 		</div>
 		<div class="bf-main-panel">
 			<div class="bf-main-heading editable-setting" data-setting-key="[<?php echo esc_attr($selected_step_code);?>][main_panel_heading]" contenteditable="true"><?php echo wp_strip_all_tags(OsStepsHelper::get_step_setting_value($selected_step_code, 'main_panel_heading')); ?></div>
 			<div class="bf-main-panel-content-wrapper">
-				<div class="bf-main-panel-content-before os-editable editable-setting" data-placeholder="+" data-setting-key="[<?php echo esc_attr($selected_step_code);?>][main_panel_content_before]"><?php echo OsStepsHelper::get_step_setting_value($selected_step_code, 'main_panel_content_before'); ?></div>
+				<div
+					class="bf-main-panel-content-before os-editable editable-setting"
+					data-placeholder="+"
+					data-setting-key="[<?php echo esc_attr( $selected_step_code ); ?>][main_panel_content_before]"
+				>
+					<?php echo wp_kses_post( OsStepsHelper::get_step_setting_value( $selected_step_code, 'main_panel_content_before' ) ); ?>
+				</div>
 				<div class="bf-main-panel-content">
 					<?php echo OsStepsHelper::get_step_content_preview($selected_step_code); ?>
 				</div>
-				<div class="bf-main-panel-content-after os-editable editable-setting" data-placeholder="+" data-setting-key="[<?php echo esc_attr($selected_step_code);?>][main_panel_content_after]"><?php echo OsStepsHelper::get_step_setting_value($selected_step_code, 'main_panel_content_after'); ?></div>
+				<div
+					class="bf-main-panel-content-after os-editable editable-setting"
+					data-placeholder="+"
+					data-setting-key="[<?php echo esc_attr( $selected_step_code ); ?>][main_panel_content_after]"
+				>
+					<?php echo wp_kses_post( OsStepsHelper::get_step_setting_value( $selected_step_code, 'main_panel_content_after' ) ); ?>
+				</div>
 			</div>
 			<div class="bf-main-panel-buttons">
                 <div class="bf-btn bf-cancel-save-btn">

Exploit Outline

The exploit involves a Cross-Site Request Forgery (CSRF) attack targeting an authenticated administrator. 1. The attacker creates a malicious webpage or sends a link that, when visited by a logged-in administrator, performs a POST request to `/wp-admin/admin-ajax.php`. 2. The request uses the `latepoint_route_call` action with the `route_name` set to `booking_form_settings__reload_preview`. 3. Because the `reload_preview` function lacks nonce validation, it processes the request and saves the provided parameters into the plugin's settings via `OsStepsHelper::save_steps_settings`. 4. The attacker includes a payload in the `steps_settings[contact][main_panel_content_before]` parameter (or similar fields like `main_panel_content_after`) containing malicious JavaScript (e.g., `<script>alert(1)</script>`). 5. These fields are not sanitized in the controller loop and are echoed without escaping in the admin preview and frontend booking form views. 6. The stored script executes whenever the booking form is loaded in the administrator's dashboard or by frontend users.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.