CVE-2026-42667

Online Scheduling and Appointment Booking System – Bookly <= 27.4 - Unauthenticated Information Exposure

mediumExposure of Sensitive Information to an Unauthorized Actor
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
27.5
Patched in
10d
Time to patch

Description

The Online Scheduling and Appointment Booking System – Bookly plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 27.4. This makes it possible for unauthenticated attackers to extract sensitive user or configuration data.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=27.4
PublishedMay 10, 2026
Last updatedMay 19, 2026

What Changed in the Fix

Changes introduced in v27.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-42667 (Bookly Information Exposure) ## 1. Vulnerability Summary The **Bookly** plugin for WordPress (versions <= 27.4) contains a sensitive information exposure vulnerability. The plugin's AJAX dispatcher improperly allows unauthenticated access to the `bookly…

Show full research plan

Exploitation Research Plan: CVE-2026-42667 (Bookly Information Exposure)

1. Vulnerability Summary

The Bookly plugin for WordPress (versions <= 27.4) contains a sensitive information exposure vulnerability. The plugin's AJAX dispatcher improperly allows unauthenticated access to the bookly_get_appointments action (and potentially related actions like bookly_get_customers). This endpoint is intended for the administrative backend to populate the appointments datatable but fails to enforce proper capability checks or authentication when registered. An unauthenticated attacker can query this endpoint to extract a JSON-formatted list of all appointments, which includes Personal Identifiable Information (PII) such as customer names, emails, phone numbers, addresses, and birthdays.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: bookly_get_appointments (inferred from backend/modules/appointments/resources/js/appointments.js which manages the bookly-appointments-datatables table).
  • HTTP Method: POST (Standard for Datatables processing).
  • Authentication: None required (vulnerable wp_ajax_nopriv_ registration or lack of check in the dispatcher).
  • Preconditions: At least one appointment must exist in the system for PII to be exposed. The [bookly-form] shortcode is usually needed on a public page to obtain a valid CSRF token.

3. Code Flow

  1. Entry Point: The attacker sends a POST request to admin-ajax.php?action=bookly_get_appointments.
  2. Dispatcher: WordPress routes the request to the wp_ajax_nopriv_bookly_get_appointments handler (or the plugin's generic AJAX dispatcher bookly_ajax if routed internally).
  3. Controller: The request reaches Bookly\Backend\Modules\Appointments\Controller::executeGetAppointments (based on the module structure in backend/modules/appointments/).
  4. Data Retrieval: The controller calls Bookly\Lib\Entities\Appointment::query() and joins with Customer and Staff entities.
  5. Sink: The data is encoded into JSON and echoed. Because the JS in appointments.js defines columns like customer.email, customer.phone, and customer.birthday, the underlying SQL query and JSON response include these fields.

4. Nonce Acquisition Strategy

Bookly uses a CSRF token (nonce) named csrf_token inside its own BooklyL10n localization object. This token is required for most AJAX actions.

  1. Identify Script Loading: The BooklyL10n object is enqueued on any page containing the Bookly booking form.
  2. Create Trigger Page: Use WP-CLI to create a public page with the booking form.
    wp post create --post_type=page --post_title="Book Now" --post_status=publish --post_content='[bookly-form]'
    
  3. Extract Token:
    • Navigate to the newly created page.
    • Use browser_eval to extract the token:
      window.BooklyL10n.csrf_token
      
    • Note: If BooklyL10n is not found, check for BooklyL10n.ajaxurl or similar objects.

5. Exploitation Strategy

Step 1: Data Acquisition

Send a crafted Datatables request to the vulnerable endpoint using the acquired csrf_token.

HTTP Request:

  • URL: http://TARGET/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=bookly_get_appointments&csrf_token=[EXTRACTED_TOKEN]&draw=1&start=0&length=10&columns[0][data]=id&columns[1][data]=customer.full_name&columns[2][data]=customer.email&columns[3][data]=customer.phone
    

Step 2: Payload Analysis

The response will be a JSON object. We are looking for the data array.
Verbatim fields from appointments.js to target in the columns parameters:

  • customer.full_name
  • customer.email
  • customer.phone
  • customer.address
  • customer.birthday

6. Test Data Setup

To verify the exposure, the environment must contain sensitive data:

  1. Create Staff: wp bookly:staff:create --full-name="Provider One" (exact CLI command may vary, use wp bookly to discover).
  2. Create Service: Create a service and link to staff.
  3. Create Customer & Appointment:
    • Manually or via CLI create a customer with PII:
      • Name: John Doe
      • Email: johndoe@example.com
      • Phone: 555-0199
      • Birthday: 1990-01-01
    • Create an appointment for this customer.
  4. Publish Form: Create the page with [bookly-form] as described in Section 4.

7. Expected Results

  • Success: The HTTP response status is 200 OK.
  • Response Body: A JSON object containing draw, recordsTotal, and a data array.
  • Exposure: The data array contains entries like:
    {
      "id": "1",
      "customer": {
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "phone": "555-0199",
        "birthday": "1990-01-01",
        "address": "..."
      }
    }
    

8. Verification Steps

  1. Check Response: Confirm the JSON contains the "John Doe" record created during setup.
  2. Database Verification: Use WP-CLI to confirm the data returned matches the database:
    wp db query "SELECT full_name, email, phone FROM wp_bookly_customers WHERE email='johndoe@example.com'"
    

9. Alternative Approaches

If bookly_get_appointments is protected:

  1. Target Customers Directly: Try action=bookly_get_customers. This endpoint populates the "Customers" tab in the backend and likely leaks the entire database of users registered in Bookly.
  2. REST API: Check for unauthenticated access to:
    • GET /wp-json/bookly/v1/appointments
    • GET /wp-json/bookly/v1/customers
      Check if these routes return data without a valid WP-Nonce header or if permission_callback is set to __return_true.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Bookly plugin for WordPress is vulnerable to unauthenticated sensitive information exposure via its AJAX dispatcher. Improper capability checks on the 'bookly_get_appointments' and 'bookly_get_customers' actions allow attackers to extract the full database of customer PII, including names, emails, phone numbers, and addresses.

Vulnerable Code

// backend/modules/appointments/resources/js/appointments.js lines 125-145
    $.each(BooklyL10n.datatables.appointments.settings.columns, function (column, show) {
        switch (column) {
            case 'customer_full_name':
                columns.push({data: 'customer.full_name', render: BooklyDatatables.escapeHtml()});
                break;
            case 'customer_phone':
                columns.push({
                    data: 'customer.phone',
                    // ...
                });
                break;
            case 'customer_email':
                columns.push({data: 'customer.email', render: BooklyDatatables.escapeHtml()});
                break;
            case 'customer_address':
                columns.push({data: 'customer.address', render: BooklyDatatables.escapeHtml(), orderable: false});
                break;
            case 'customer_birthday':
                columns.push({data: 'customer.birthday', render: BooklyDatatables.escapeHtml()});
                break;

---

// backend/modules/appointments/resources/js/appointments.js lines 251-267
    let options = {
        ajax: {
            url: ajaxurl,
            type: 'POST',
            data: function (d) {
                return $.extend({}, d, {
                    action: 'bookly_get_appointments',
                    csrf_token: BooklyL10n.csrf_token,
                    filter: {
                        id: $idFilter.val(),
                        // ...
                    }
                });
            }
        },

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.4/backend/modules/appointments/resources/js/appointments.js /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.5/backend/modules/appointments/resources/js/appointments.js
--- /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.4/backend/modules/appointments/resources/js/appointments.js	2026-04-14 11:21:56.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.5/backend/modules/appointments/resources/js/appointments.js	2026-04-22 12:53:42.000000000 +0000
@@ -120,6 +120,8 @@
     /**
      * Init table columns.
      */
+    const table = 'appointments';
+
     $.each(BooklyL10n.datatables.appointments.settings.columns, function (column, show) {
         switch (column) {
             case 'customer_full_name':
@@ -155,7 +157,7 @@
                     render: function (data, type, row, meta) {
                         data = BooklyDatatables.escapeHtml(data);
                         if (row.service.extras.length) {
-                            var extras = '<ul class="bookly-list list-dots">';
+                            var extras = '<ul class="bookly-list list-dots bookly:m-0">';
                             $.each(row.service.extras, function (key, item) {
                                 extras += '<li><nobr>' + item.title + '</nobr></li>';
                             });
@@ -202,7 +204,7 @@
                         if (row.rating_comment == null) {
                             return row.rating;
                         } else {
-                            return '<a href="#" data-toggle="bookly-popover" data-trigger="hover" data-placement="bottom" data-content="' + BooklyDatatables.escapeHtml(row.rating_comment) + '" data-container="#bookly-appointments-datatables">' + $.fn.dataTable.render.text().display(row.rating) + '</a>';
+                            return '<a href="#" data-toggle="bookly-popover" data-trigger="hover" data-placement="bottom" data-content="' + BooklyDatatables.escapeHtml(row.rating_comment) + '" data-container="#bookly-appointments-datatables">' + BooklyDatatables.escapeHtml(row.rating) + '</a>';
                         }
                     },
                 });
@@ -226,6 +228,8 @@
                                 return '<a class="badge badge-primary" href="' + BooklyDatatables.escapeHtml(row.online_meeting_start_url) + '" target="_blank"><i class="fas fa-video fa-fw"></i> Jitsi Meet <i class="fas fa-external-link-alt fa-fw"></i></a>';
                             case 'bbb':
                                 return '<a class="badge badge-primary" href="' + BooklyDatatables.escapeHtml(row.online_meeting_start_url) + '" target="_blank"><i class="fas fa-video fa-fw"></i> BigBlueButton <i class="fas fa-external-link-alt fa-fw"></i></a>';
+                            case 'teams':
+                                return '<a class="badge badge-primary" href="' + BooklyDatatables.escapeHtml(row.online_meeting_start_url) + '" target="_blank"><i class="fas fa-video fa-fw"></i> Microsoft Teams <i class="fas fa-external-link-alt fa-fw"></i></a>';
                             default:
                                 return '';
                         }
@@ -247,12 +251,11 @@
                 }
                 break;
         }        
-        columns[columns.length - 1].title = BooklyL10n.datatables.appointments.titles[column] || column;
+        columns[columns.length - 1].title = BooklyL10n.datatables[table].titles[column] || column;
         columns[columns.length - 1].name = column;
         columns[columns.length - 1].show = show;
     });
 
-    let table = 'appointments';
     let options = {
         ajax: {
             url: ajaxurl,

Exploit Outline

The exploit leverages the lack of server-side capability checks on Bookly's AJAX datatable endpoints. 1. **Nonce Acquisition**: An attacker visits any public page containing a Bookly booking form (shortcode `[bookly-form]`). They extract the `csrf_token` from the global `BooklyL10n` JavaScript object present in the page source. 2. **Target Endpoint**: The attacker targets `/wp-admin/admin-ajax.php` with a POST request. 3. **Payload Construction**: The request uses the action `bookly_get_appointments`. The payload includes the extracted `csrf_token` and standard Datatables parameters (`draw`, `start`, `length`). The attacker defines columns in the payload (e.g., `columns[1][data]=customer.email`) to specify which sensitive fields to return. 4. **Data Extraction**: The server responds with a JSON object containing the `data` array, which includes the PII (names, emails, phones, birthdays) of all customers stored in the booking system. This process can be repeated for the `bookly_get_customers` action to leak the entire customer directory.

Check if your site is affected.

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