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
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:NTechnical Details
What Changed in the Fix
Changes introduced in v5.2.8
Source Code
WordPress.org SVN# 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 othermain_panel_contentfields). - 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
- Entry Point: The request hits
admin-ajax.phpwithaction=latepoint_route_call. - Routing: The LatePoint router (likely in
OsRouterHelper) dispatches the request toOsBookingFormSettingsController::reload_preview(). - Missing Security: Unlike other methods (e.g.,
OsCustomersController::destroy()),reload_preview()does not call$this->check_nonce(). - Processing Loop:
- The function iterates through
steps_settings. - For specific keys like
side_panel_headingorside_panel_description, it applieswp_strip_all_tagsorstrip_tags. - Crucially, keys like
main_panel_content_beforeandmain_panel_content_afterare 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 ); } } }
- The function iterates through
- Sink: The unsanitized settings are saved to the database via
OsStepsHelper::save_steps_settings( $steps_settings );. - 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.
- Identify Script Context: LatePoint settings are managed in the admin dashboard under "Booking Form Settings".
- Navigation: Navigate to
wp-admin/admin.php?page=latepoint&route_name=booking_form_settings__preview. - Extraction: Use
browser_evalto extract the admin nonce from thelatepoint_helperglobal object (common in LatePoint).- Target Variable:
window.latepoint_helper?.nonceorwindow.latepoint_admin_params?.nonce.
- Target Variable:
- 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
Usebrowser_navigateto the LatePoint dashboard, thenbrowser_evalto grab the nonce.Step 2: CSRF POST Request
Use thehttp_requesttool 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>
- Method:
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
- Enable Plugin: Ensure
latepointis active. - 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]'
- Identify Step: The "Contact" step (
contact) is standard in most LatePoint configurations.
7. Expected Results
- The
admin-ajax.phprequest should return a JSON response with"status": "success"and includebooking_form_htmlcontaining the injected script. - Upon visiting the "Booking" page created in setup, a browser alert
CVE-2026-2324_XSSshould 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 thecontact -> main_panel_content_beforekey 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_previewfunction also processesparams['steps_settings']['shared']and callsOsSettingsHelper::save_setting_by_name. Ifmain_panel_content_beforeis filtered, try injecting viasteps_support_textwhich allows several HTML tags (thoughscriptis stripped viastrip_tags). - General Settings Sink: Use
params['settings']to overwrite other plugin settings ifsteps_settingsare sanitized in a specific environment. The loop overparams['settings']callsOsSettingsHelper::prepare_valuewhich might be less restrictive for custom setting names.
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
@@ -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(); @@ -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.