CVE-2026-32540

Online Scheduling and Appointment Booking System – Bookly <= 26.7 - Reflected Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.1
CVSS Score
6.1
CVSS Score
medium
Severity
26.8
Patched in
7d
Time to patch

Description

The Online Scheduling and Appointment Booking System – Bookly plugin for WordPress is vulnerable to Reflected Cross-Site Scripting in versions up to, and including, 26.7 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user 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<=26.7
PublishedMarch 20, 2026
Last updatedMarch 26, 2026

What Changed in the Fix

Changes introduced in v26.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

SS in `staff_id` parameter of the `bookly-calendar` admin page. 2. **Vector:** `GET /wp-admin/admin.php?page=bookly-calendar&staff_id=[PAYLOAD]` 3. **Sink:** The value is reflected into the `BooklyL10n` JS object or rendered in a dropdown via the `Select` component without proper escaping.…

Show full research plan

SS in staff_id parameter of the bookly-calendar admin page.
2. Vector: GET /wp-admin/admin.php?page=bookly-calendar&staff_id=[PAYLOAD]
3. Sink: The value is reflected into the BooklyL10n JS object or rendered in a dropdown via the Select component without proper escaping.
4. Payload: "><script>alert(document.domain)</script>
5. Steps:
- Create Admin.
- Create Staff.
- Request URL with payload.
- Observe script execution in the browser.

*Self-Correction*: Since the agent is automated, I should give it a few targets to check. `staff_id` and `view` are the most likely.

*Verification*: The `Select.php` component is definitely a sink. I should mention it.

*Nonce Strategy*: Since it's a reflected XSS in the admin area via a GET request, no nonce is typically required to *trigger* it, but the victim must be logged in.

*Wait*, look at `backend/modules/appointments/Ajax.php` again.
`getAppointments` is called via AJAX.
Reflected XSS can also happen in AJAX responses if the response is `text/html`.
But `wp_send_json` is used.

Let's stick to the `Page.php`
Research Findings
Static analysis — not yet PoC-verified

Summary

The Bookly plugin for WordPress (<= 26.7) is vulnerable to Reflected Cross-Site Scripting (XSS) via the 'staff_id' and 'view' parameters on the calendar admin page. This occurs due to insufficient output escaping when these values are reflected into the 'BooklyL10n' JavaScript object or rendered in dropdown components using an inadequate 'stripScripts' filter.

Vulnerable Code

/* backend/components/schedule/Select.php */

// Lines 98-113
        foreach ( $this->values as $option_value => $option_text ) {
            if ( $value_added === false ) {
                if ( $this->value == $option_value ) {
                    $value_added = true;
                } elseif ( $this->value < $option_value ) {
                    // Make sure that value presents in the list,
                    // even if corresponding option did not exist in $this->values.
                    $options .= sprintf(
                        '<option value="%s" selected="selected">%s</option>',
                        $this->value,
                        Lib\Utils\DateTime::formatTime( Lib\Utils\DateTime::timeToSeconds( $this->value ) )
                    );
                    $value_added = true;
                }
            }
            $options .= sprintf(
                '<option value="%s"%s>%s</option>',
                $option_value,
                selected( $this->value, $option_value, false ),
                $option_text
            );
        }

        $html = sprintf( '<select %s data-default_value="%s">%s</select>',
            $attributes_str,
            $this->value,
            $options
        );

        if ( $echo ) {
            echo Lib\Utils\Common::stripScripts( $html );
        } else {
            return $html;
        }

---

/* backend/modules/calendar/Page.php */

// Lines 76-78
        wp_localize_script( 'bookly-calendar.js', 'BooklyL10n', array_merge(
            Lib\Utils\Common::getCalendarSettings(),
            array(

Security Fix

diff -ru /26.7/backend/components/schedule/Select.php /26.8/backend/components/schedule/Select.php
--- /26.7/backend/components/schedule/Select.php	2026-01-22 14:02:08.000000000 +0000
+++ /26.8/backend/components/schedule/Select.php	2026-02-24 10:08:40.000000000 +0000
@@ -40,7 +40,7 @@
 
         // Insert empty value if required.
         if ( $options['use_empty'] ) {
-            $this->values[ null ] = $options['empty_value'];
+            $this->values[''] = $options['empty_value'];
         }
 
         $ts_length  = Lib\Config::getTimeSlotLength();
diff -ru /26.7/backend/modules/calendar/Page.php /26.8/backend/modules/calendar/Page.php
--- /26.7/backend/modules/calendar/Page.php	2026-01-22 14:02:08.000000000 +0000
+++ /26.8/backend/modules/calendar/Page.php	2026-02-24 10:08:40.000000000 +0000
@@ -16,8 +16,10 @@
      */
     public static function render()
     {
+        $calendar_version = get_option( 'bookly_legacy_calendar' ) ? 'legacy' : 'latest';
+
         self::enqueueStyles( array(Custom
-            'module' => array( 'css/event-calendar.min.css' => array( 'bookly-backend-globals' ) ),
+            'module' => array( 'css/' . ( $calendar_version !== 'latest' ? 'event-calendar-4.min.css' : 'event-calendar.min.css' ) => array( 'bookly-backend-globals' ) ),
         ) );
 
         $id = Lib\Entities\Appointment::query()->fetchVar( 'MAX(id)' );
@@ -76,6 +78,7 @@
         wp_localize_script( 'bookly-calendar.js', 'BooklyL10n', array_merge(
             Lib\Utils\Common::getCalendarSettings(),
             array(
+                'calendar_version' => $calendar_version,
                 'delete' => __( 'Delete', 'bookly' ),
                 'are_you_sure' => __( 'Are you sure?', 'bookly' ),
                 'filterResourcesWithEvents' => Config::showOnlyStaffWithAppointmentsInCalendarDayView(),

Exploit Outline

1. An attacker crafts a malicious URL targeting the Bookly calendar page: `/wp-admin/admin.php?page=bookly-calendar&staff_id="><script>alert(document.domain)</script>`. 2. The attacker tricks a logged-in administrator or staff member into clicking this link. 3. The `staff_id` parameter is processed by the plugin and reflected into the `BooklyL10n` global JavaScript object or used to populate a `Select` component. 4. Due to missing `esc_attr` or `esc_html` escaping, and the failure of the `stripScripts` function to neutralize the payload, the injected `<script>` tag is executed in the victim's browser context. 5. The script can then perform actions on behalf of the user, such as stealing session cookies or creating new administrative users.

Check if your site is affected.

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