CVE-2026-2519

Online Scheduling and Appointment Booking System – Bookly <= 27.0 - Unauthenticated Price Manipulation via 'tips'

mediumExternal Control of Assumed-Immutable Web Parameter
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
27.1
Patched in
2d
Time to patch

Description

The Online Scheduling and Appointment Booking System – Bookly plugin for WordPress is vulnerable to price manipulation via the 'tips' parameter in all versions up to, and including, 27.0. This is due to the plugin trusting a user-supplied input without server-side validation against the configured price. This makes it possible for unauthenticated attackers to submit a negative number to the 'tips' parameter, causing the total price to be reduced to zero.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=27.0
PublishedApril 8, 2026
Last updatedApril 9, 2026

What Changed in the Fix

Changes introduced in v27.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-2519 - Bookly Price Manipulation ## 1. Vulnerability Summary The **Online Scheduling and Appointment Booking System – Bookly** plugin (versions <= 27.0) contains a logic flaw where user-supplied input for the `tips` parameter is trusted without server-side val…

Show full research plan

Exploitation Research Plan: CVE-2026-2519 - Bookly Price Manipulation

1. Vulnerability Summary

The Online Scheduling and Appointment Booking System – Bookly plugin (versions <= 27.0) contains a logic flaw where user-supplied input for the tips parameter is trusted without server-side validation. Specifically, the plugin fails to ensure that the tip amount is a non-negative value. An unauthenticated attacker can submit a negative value for the tips parameter during the booking process, which is then added to the total service price, effectively reducing the final amount to zero and allowing free bookings of paid services.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: bookly_render_payment (or bookly_checkout)
  • Vulnerable Parameter: tips
  • Authentication: Unauthenticated (accessible via wp_ajax_nopriv_ handlers)
  • Preconditions:
    1. Bookly plugin must be active.
    2. A service with a non-zero price must be configured.
    3. A page must contain the Bookly booking form shortcode: [bookly-form].

3. Code Flow

  1. The booking process is managed by Bookly\Lib\UserBookingData.
  2. During the "Payment" step of the booking wizard, the frontend sends an AJAX request to update the user's booking session.
  3. The property protected $tips; in lib/UserBookingData.php (line 104) is populated from the request.
  4. The $properties array (line 144) includes 'tips', indicating it is a dynamically settable property from user input.
  5. In the calculation phase (likely in Bookly\Lib\Cart::getInfo()), the plugin sums the service price and the tips value.
  6. Because the code lacks a check like max( 0, $tips ), a negative value (e.g., -50.00) is subtracted from the total.

4. Nonce Acquisition Strategy

Bookly uses a CSRF token (nonce) for all its AJAX operations. This token is typically localized for the frontend.

  1. Identify Shortcode: The plugin uses [bookly-form].
  2. Create Test Page:
    wp post create --post_type=page --post_title="Booking" --post_status=publish --post_content='[bookly-form]'
  3. Navigate and Extract:
    Navigate to the new page.
    The nonce is stored in a JavaScript object localized by the plugin, usually bookly_ajax or BooklyL10n.
  4. Browser Eval:
    Use browser_eval("bookly_options.csrf_token") to retrieve the token.

5. Exploitation Strategy

The goal is to proceed through the booking steps and inject a negative tip that zeroes out the total price.

Step 1: Initialize Session and Progress Steps

Bookly uses a multi-step AJAX process. You must simulate moving through the steps.

  • Action 1 (Service Selection): action=bookly_render_time
  • Action 2 (Time Selection): action=bookly_render_details
  • Action 3 (User Details): action=bookly_render_payment

Step 2: Inject Negative Tip

When calling the step that calculates or renders the payment, or during the final checkout, include the negative tips parameter.

HTTP Request (Example):

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=bookly_render_payment&
csrf_token=[NONCE]&
form_id=[FORM_ID]&
tips=-100.00&
...[other session parameters]...

Step 3: Finalize Checkout

If the total price is reduced to zero, the plugin may allow the "Local Payment" (or skip payment) to succeed even for premium services.

HTTP Request (Checkout):

POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

action=bookly_checkout&
csrf_token=[NONCE]&
form_id=[FORM_ID]&
tips=-100.00

6. Test Data Setup

  1. Staff Member: Create a staff member.
  2. Service: Create a service named "Premium Consulting" with a price of 100.00.
  3. Page: Create a page with [bookly-form].
  4. Settings: Ensure at least one payment method is active (e.g., "Local Payment").

7. Expected Results

  • The bookly_render_payment response should return HTML/JSON showing a "Total" of 0.00.
  • The bookly_checkout response should indicate success ("success": true) and create an appointment without requiring a valid payment gateway transaction.

8. Verification Steps

  1. Check Database: Use wp db query "SELECT * FROM wp_bookly_payments ORDER BY id DESC LIMIT 1;".
  2. Verify Total: Confirm the total column in the database for the new payment record is 0.00 despite the service price being 100.00.
  3. Check Tip Value: Confirm the tips column contains the negative value (e.g., -100.00).

9. Alternative Approaches

If bookly_render_payment does not allow the manipulation, attempt the injection at the bookly_save_appointment_details action or directly during the bookly_checkout action.

If the form uses a different JS variable, use browser_eval("Object.keys(window).filter(k => k.includes('bookly'))") to find the correct localization object containing the csrf_token.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Bookly plugin for WordPress is vulnerable to price manipulation in versions up to and including 27.0 because it trusts user-supplied input for the 'tips' parameter without server-side validation. An unauthenticated attacker can submit a negative value for this parameter during the checkout process, which is subtracted from the total service cost, allowing them to book appointments for free.

Vulnerable Code

// lib/UserBookingData.php:104
    /** @var float */
    protected $tips;

---

// lib/UserBookingData.php:144
        // Step payment
        'coupon_code',
        'gift_code',
        'tips',
        'deposit_full',

---

// lib/UserBookingData.php:361
    public function fillData( array $data )
    {
        foreach ( $data as $name => $value ) {
            if ( in_array( $name, $this->properties ) ) {
                $this->{$name} = $value;
            } elseif ( $name == 'chain' ) {

---

// lib/UserBookingData.php (inferred from patch diff around line 1898)
    public function setTips( $tips )
    {
        $this->tips = $tips;

        return $this;
    }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.0/lib/UserBookingData.php /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.1/lib/UserBookingData.php
--- /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.0/lib/UserBookingData.php	2026-03-06 09:39:24.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/bookly-responsive-appointment-booking-tool/27.1/lib/UserBookingData.php	2026-03-12 08:49:26.000000000 +0000
@@ -144,7 +144,6 @@
         // Step payment
         'coupon_code',
         'gift_code',
-        'tips',
         'deposit_full',
         // Cart item keys being edited
         'edit_cart_keys',
@@ -361,7 +360,9 @@
     public function fillData( array $data )
     {
         foreach ( $data as $name => $value ) {
-            if ( in_array( $name, $this->properties ) ) {
+            if ( $name === 'tips' ) {
+                $this->setTips( $value );
+            } elseif ( in_array( $name, $this->properties ) ) {
                 $this->{$name} = $value;
             } elseif ( $name == 'chain' ) {
                 $chain_items = $this->chain->getItems();
@@ -1898,7 +1899,7 @@
      */
     public function setTips( $tips )
     {
-        $this->tips = $tips;
+        $this->tips = max( 0, (float) $tips );
 
         return $this;
     }

Exploit Outline

The exploit targets the AJAX booking flow of the Bookly plugin. An unauthenticated attacker first obtains a valid CSRF token (`csrf_token`) from the localized JavaScript on any page containing the `[bookly-form]` shortcode. The attacker then initiates a multi-step booking process via `admin-ajax.php`. During the step that renders payment information or finalizes the checkout (actions like `bookly_render_payment` or `bookly_checkout`), the attacker includes the `tips` parameter in the POST request with a negative value (e.g., `tips=-50.00`). Because the server lacks validation on this input and uses a setter that directly assigns the value, the negative 'tip' is added to the total, effectively subtracting from the service price. If the total is reduced to zero, the plugin allows the appointment to be finalized without a valid payment transaction.

Check if your site is affected.

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