CVE-2026-2231

Fluent Booking <= 2.0.01 - Unauthenticated Stored Cross-Site Scripting via Multiple Parameters

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
2.0.05
Patched in
2d
Time to patch

Description

The Fluent Booking plugin for WordPress is vulnerable to Stored Cross-Site Scripting via multiple parameters in all versions up to, and including, 2.0.01 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.0.01
PublishedMarch 25, 2026
Last updatedMarch 26, 2026
Affected pluginfluent-booking

What Changed in the Fix

Changes introduced in v2.0.05

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan targets **CVE-2026-2231**, an unauthenticated stored Cross-Site Scripting (XSS) vulnerability in the Fluent Booking plugin. ### 1. Vulnerability Summary The Fluent Booking plugin (up to version 2.0.01) fails to sufficiently sanitize and escape attendee-provided data during the bo…

Show full research plan

This research plan targets CVE-2026-2231, an unauthenticated stored Cross-Site Scripting (XSS) vulnerability in the Fluent Booking plugin.

1. Vulnerability Summary

The Fluent Booking plugin (up to version 2.0.01) fails to sufficiently sanitize and escape attendee-provided data during the booking process. Specifically, when an unauthenticated user schedules a meeting via the wp_ajax_nopriv_fluent_cal_schedule_meeting AJAX action, several input parameters (like name, message, and custom fields) are stored in the database. While some parameters undergo basic sanitization (e.g., sanitize_text_field), the data is later rendered in the admin "Schedules" dashboard and potentially in export files (CSV/JSON) without proper output escaping. This allows an attacker to inject arbitrary scripts that execute in the context of an administrative user.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: fluent_cal_schedule_meeting (specifically the nopriv version)
  • Vulnerable Parameters: name, message, address, phone_number, location_description, and custom_fields.
  • Authentication: Unauthenticated (no account required).
  • Preconditions: A Calendar and at least one active Calendar Slot (Event) must exist.

3. Code Flow

  1. Entry Point: FrontEndHandler::register() defines wp_ajax_nopriv_fluent_cal_schedule_meeting which routes to ajaxScheduleMeeting.
  2. Processing: BookingController::createBooking() is invoked.
  3. Data Extraction: The controller gathers input via $request->all().
  4. Sanitization (Insufficient):
    • name is passed through sanitize_text_field().
    • message, phone, and address are passed through sanitize_textarea_field().
    • custom_fields are processed by BookingFieldService::getCustomFieldsData().
  5. Persistence: Data is saved to the fcal_bookings table via the Booking model.
  6. Sink (Rendering):
    • Admin dashboard: SchedulesController::index() returns booking data in JSON format. The React/Vue frontend renders these values (e.g., message) potentially using unsafe methods like v-html or simply failing to escape within the DOM.
    • Data Exports: DataExporter::exportBookingHosts() writes raw model values to a CSV.

4. Nonce Acquisition Strategy

The booking form requires a nonce for the action fluent_cal_schedule_meeting. This nonce is localized into the page where the booking shortcode is present.

  1. Identify Shortcode: The plugin uses [fluent_booking id="XX"] or [fluent_booking hash="YY"].
  2. Setup Page: Create a page containing a valid booking shortcode.
  3. Navigate: Use the browser tool to visit this page.
  4. Extract Nonce: The plugin localizes data into a global JavaScript variable named fcal_public_vars_{calendar_id}_{event_id}.
  5. JS Command:
    // Example for Calendar 1, Event 1
    window.fcal_public_vars_1_1?.nonce || window.fcal_public_vars_1_1?._fcal_token
    
    Note: Based on FrontEndHandler.php, the localized variable name is strictly fcal_public_vars_ + ID + _ + EventID.

5. Exploitation Strategy

  1. Discovery:
    • Find an active event_id and its corresponding calendar_id.
    • Find an available time slot for that event by calling the nopriv_fluent_cal_get_available_dates action or nopriv_fluent_cal_get_available_slots.
  2. Payload Preparation: Use a standard XSS payload that survives sanitize_text_field (which strips tags but can be bypassed if the sink is an attribute or if the sanitization is skipped for specific fields).
    • Primary: <img src=x onerror=alert(document.domain)>
    • Attribute Breakout: "><svg/onload=alert(1)>
  3. Submission:
    • Send a POST request to admin-ajax.php.
    • Action: fluent_cal_schedule_meeting
    • Required Fields: name, email, timezone, event_time, _fcal_token.
    • Payload Fields: Inject into name, message, and address.

6. Test Data Setup

  1. Create Calendar: Use WP-CLI to ensure at least one calendar exists.
    wp eval "FluentBooking\App\Models\Calendar::create(['title' => 'Test Calendar', 'user_id' => 1, 'type' => 'simple', 'status' => 'active']);"
    
  2. Create Event (Slot):
    wp eval "FluentBooking\App\Models\CalendarSlot::create(['calendar_id' => 1, 'title' => 'Consultation', 'slug' => 'consult', 'status' => 'active', 'duration' => 30, 'event_type' => 'one_on_one']);"
    
  3. Create Public Page:
    wp post create --post_type=page --post_title="Book Here" --post_content='[fluent_booking id="1"]' --post_status=publish
    

7. Expected Results

  • The ajaxScheduleMeeting request should return 200 OK or 201 Created with a success message.
  • When an administrator navigates to Fluent Booking > Schedules, the injected script in the name or message field should execute immediately.

8. Verification Steps

  1. Check Database:
    wp db query "SELECT name, message FROM wp_fcal_bookings ORDER BY id DESC LIMIT 1;"
    
  2. Verify Admin Execution: Use browser_navigate to http://localhost:8080/wp-admin/admin.php?page=fluent-booking#/schedules and check for an alert or a specific DOM change.

9. Alternative Approaches

  • Custom Fields: If the main fields are sanitized, target custom_fields. In version 2.0.01, the custom field processing in BookingFieldService may not strictly sanitize all input types.
    • Parameter: custom_fields[1]=<script>alert(1)</script>
  • UTM Parameters: The plugin captures UTM data (utm_source, utm_medium). These are often overlooked and rendered in the "Attendee Info" section of the schedule.
    • Parameter: utm_source=<img src=x onerror=alert(1)>
  • Export Injection: If the XSS doesn't trigger in the dashboard, check the CSV export functionality in DataExporter::exportBookingHosts. Inject =cmd|' /C calc'!A1 to test for CSV injection as well.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Fluent Booking plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting (XSS) due to insufficient sanitization and escaping of attendee-provided data such as names, messages, and UTM parameters. Attackers can inject arbitrary scripts during the booking process that execute when an administrator views the scheduling dashboard or exports booking data.

Vulnerable Code

// app/Hooks/Handlers/FrontEndHandler.php:840
        $bookingData = apply_filters('fluent_booking/initialize_booking_data', [
            'person_time_zone' => sanitize_text_field($timezone),
            'start_time'       => $startDateTime,
            'name'             => sanitize_text_field($postedData['name']),
            'email'            => sanitize_email($postedData['email']),
            'message'          => sanitize_textarea_field(wp_unslash(Arr::get($postedData, 'message', ''))),
            'phone'            => sanitize_textarea_field(Arr::get($postedData, 'phone_number', '')),
            'address'          => sanitize_textarea_field(Arr::get($postedData, 'address', '')),
            'ip_address'       => Helper::getIp(),
            'status'           => 'scheduled',
            'source'           => 'web',
            'event_type'       => $calendarEvent->event_type,
            'slot_minutes'     => $duration,
            'utm_source'       => SanitizeService::sanitizeUtmData(Arr::get($postedData, 'utm_source', '')),
            'utm_medium'       => SanitizeService::sanitizeUtmData(Arr::get($postedData, 'utm_medium', '')),
            'utm_campaign'     => SanitizeService::sanitizeUtmData(Arr::get($postedData, 'utm_campaign', '')),
            'utm_term'         => SanitizeService::sanitizeUtmData(Arr::get($postedData, 'utm_term', '')),
            'utm_content'      => SanitizeService::sanitizeUtmData(Arr::get($postedData, 'utm_content', ''))
        ], $postedData, $calendarEvent);

---

// app/Hooks/Handlers/DataExporter.php:83
        foreach ($attendees as $attendee) {
            $row = [
                $attendee->first_name,
                $attendee->last_name,
                $attendee->email,
                $attendee->message,
                $attendee->getLocationAsText(),
                $attendee->source,
                $attendee->booking_type,
                $attendee->status,
                $attendee->source_url,
                $attendee->slot_minutes,
                $attendee->start_time,
                $attendee->end_time,
                $attendee->payment_status,
            ];

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/fluent-booking/2.0.01/app/Hooks/Handlers/DataExporter.php /home/deploy/wp-safety.org/data/plugin-versions/fluent-booking/2.0.05/app/Hooks/Handlers/DataExporter.php
--- /home/deploy/wp-safety.org/data/plugin-versions/fluent-booking/2.0.01/app/Hooks/Handlers/DataExporter.php	2026-01-01 11:54:56.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/fluent-booking/2.0.05/app/Hooks/Handlers/DataExporter.php	2026-02-17 13:13:46.000000000 +0000
@@ -80,36 +88,36 @@
 
         foreach ($attendees as $attendee) {
             $row = [
-                $attendee->first_name,
-                $attendee->last_name,
-                $attendee->email,
-                $attendee->message,
-                $attendee->getLocationAsText(),
-                $attendee->source,
-                $attendee->booking_type,
-                $attendee->status,
-                $attendee->source_url,
-                $attendee->slot_minutes,
-                $attendee->start_time,
-                $attendee->end_time,
-                $attendee->payment_status,
+                $this->sanitizeCsvCell($attendee->first_name),
+                $this->sanitizeCsvCell($attendee->last_name),
+                $this->sanitizeCsvCell($attendee->email),
+                $this->sanitizeCsvCell($attendee->message),
+                $this->sanitizeCsvCell($attendee->getLocationAsText()),
+                $this->sanitizeCsvCell($attendee->source),
+                $this->sanitizeCsvCell($attendee->booking_type),
+                $this->sanitizeCsvCell($attendee->status),
+                $this->sanitizeCsvCell($attendee->source_url),
+                $this->sanitizeCsvCell($attendee->slot_minutes),
+                $this->sanitizeCsvCell($attendee->start_time),
+                $this->sanitizeCsvCell($attendee->end_time),
+                $this->sanitizeCsvCell($attendee->payment_status),
             ];
 
             if ($attendee->payment_status) {
                 $order = $attendee->payment_order;
                 if ($order) {
                     $order->load(['items', 'transaction']);
-                    $row[] = $order->status;
-                    $row[] = $order->payment_method;
-                    $row[] = $order->currency;
+                    $row[] = $this->sanitizeCsvCell($order->status);
+                    $row[] = $this->sanitizeCsvCell($order->payment_method);
+                    $row[] = $this->sanitizeCsvCell($order->currency);
                     $row[] = $order->total_amount / 100;
-                    $row[] = $order->created_at;
-                    $row[] = $order->transaction->id;
-                    $row[] = $order->transaction->vendor_charge_id;
-                    $row[] = $order->transaction->payment_method;
-                    $row[] = $order->transaction->status;
+                    $row[] = $this->sanitizeCsvCell($order->created_at);
+                    $row[] = $this->sanitizeCsvCell($order->transaction->id);
+                    $row[] = $this->sanitizeCsvCell($order->transaction->vendor_charge_id);
+                    $row[] = $this->sanitizeCsvCell($order->transaction->payment_method);
+                    $row[] = $this->sanitizeCsvCell($order->transaction->status);
                     $row[] = $order->transaction->total / 100;
-                    $row[] = $order->transaction->created_at;
+                    $row[] = $this->sanitizeCsvCell($order->transaction->created_at);
                 }
             } else {

Exploit Outline

To exploit this vulnerability, an unauthenticated attacker first obtains a valid public booking nonce (usually found in the localized JavaScript of any page containing the `[fluent_booking]` shortcode). The attacker then sends a POST request to the `wp_ajax_nopriv_fluent_cal_schedule_meeting` AJAX endpoint. The payload includes standard booking fields like `name`, `email`, and `event_time`, but injects XSS vectors (e.g., `<script>alert(1)</script>` or `<img src=x onerror=alert(1)>`) into parameters such as `message`, `utm_source`, or custom fields. Once submitted, the malicious script is stored in the database and executes in the context of an administrator's browser session when they navigate to the Fluent Booking 'Schedules' page or export the booking data.

Check if your site is affected.

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