During independent security research conducted as part of the Wordfence Bug Bounty Program, we identified an unauthenticated Insecure Direct Object Reference (IDOR) vulnerability in Bookly, a WordPress appointment booking plugin used to manage online scheduling, services, staff availability and customer appointments. Bookly is actively used across more than 60,000 WordPress installations and provides a public-facing booking workflow that allows customers to select services, choose available appointment times and complete bookings directly through a website.
The issue was identified within this booking workflow and originated from the way Bookly handled order information across multiple frontend AJAX actions. An attacker-controlled order ID could be stored inside a newly created booking session and later trusted by another part of the application, causing Bookly to disclose the secret token associated with an unrelated customer’s order. That token could then be reused against additional unauthenticated functionality to retrieve calendar information associated with the victim booking and, where the relevant payment had not been completed, permanently remove the booking.
The vulnerability affected Bookly versions up to and including 28.2 and was addressed in version 28.3. It was responsibly disclosed through Wordfence and assigned CVE-2026-93399. What made the issue particularly interesting was that the initial weakness did not immediately provide the final impact. The severity only became clear after following the attacker-controlled order ID through the rest of Bookly’s booking workflow and identifying where the resulting order token was trusted elsewhere in the application. This article explains that chain, the underlying access-control failure and how the vulnerability could be reproduced in practice.
Understanding the Bookly Booking Workflow
Before looking at the vulnerability itself, it is useful to understand how Bookly keeps track of a booking as a customer moves through the frontend workflow.
Bookly allows unauthenticated visitors to select a service, choose an appointment time and progress through the booking process without requiring a WordPress account. To maintain state between each stage, the plugin uses a generated form_id that represents the current booking session and allows subsequent AJAX requests to continue working with the same booking data.
As part of this workflow, Bookly creates an Order representing the booking transaction. Each Order is assigned a numeric order_id, which is used internally to identify the corresponding record. During testing, these identifiers were observed to be sequential.
Alongside this internal identifier, Bookly assigns each Order a separate non-sequential token, exposed within the affected workflow as bookly_order. Rather than requiring later frontend actions to operate directly on the numeric order_id, this token is used as the reference to the Order when performing actions such as calendar export and order rollback.
This separation forms an important part of the security model. The numeric identifier is used internally to reference the Order, while the separate token is relied upon by frontend functionality capable of interacting with the associated booking. In practice, knowing or predicting the internal identifier should not be enough to reach functionality that relies on possession of the corresponding token. That distinction became particularly interesting during testing. While tracing how Bookly carried order information between different stages of the booking process, it became apparent that the boundary between the predictable internal identifier and the secret order token was not as strong as it first appeared.
Identifying the Vulnerability
Creating a Session with an Arbitrary Order ID
The first point of interest was the unauthenticated bookly_get_form_id AJAX action, which is used to create and restore the state associated with a Bookly booking form. Alongside generating a new form_id, the action accepts a client-supplied json_data structure containing information about the current booking flow. Within this structure, Bookly also accepted the order_id.
This was significant because the value was not simply used for display or passed back to the client. When a new booking session was created, the supplied order_id was persisted as part of that session’s state. Bookly did not establish that the referenced Order had actually been created through the session being initialised. As a result, an unauthenticated user could start a completely new Bookly session while supplying the numeric identifier of an Order created through an unrelated booking. Bookly would return a legitimate form_id for the new session, but the state associated with it now referenced somebody else’s Order.
At this point, the behaviour was interesting, but the immediate impact was still limited. We had influenced which Order the session referred to, but had not yet gained access to the booking itself. The next stage of the workflow was what turned that state manipulation into something considerably more useful.
From Session State to Order Token
The next step was to take the form_id returned by bookly_get_form_id and submit it to Bookly’s bookly_render_complete action. This action is used at the end of the booking process to restore the existing session and render the completion response for the Order associated with it.
Because the session had been created using another customer’s order_id, the state tied to our form_id now referenced that customer’s Order. When bookly_render_complete restored the session, it trusted that reference and loaded the corresponding Order without confirming that it had actually been created through our booking flow. The response then returned the bookly_order token associated with that Order.
This was the point at which the impact changed significantly. We had moved from controlling a predictable numeric identifier to obtaining the separate secret token that Bookly relied upon when exposing later frontend functionality for the booking. The issue was therefore not simply that an arbitrary order_id could be introduced into the booking flow. The more important flaw was that a client-controlled reference introduced earlier in the booking process was later treated as trusted application state, allowing our session to resolve to another customer’s Order and disclose its token. With the bookly_order token now exposed, the next step was to determine what other parts of Bookly relied on it.
Following the Token
Recovering the bookly_order token was only part of the issue. The next step was to identify where else Bookly accepted that value and whether possession of the token exposed any additional functionality associated with the booking. Two unauthenticated AJAX actions were particularly relevant: bookly_add_to_calendar and bookly_rollback_order.
Calendar Disclosure
The bookly_add_to_calendar action allows booking information to be exported as an ICS calendar entry. The action accepts a bookly_order token to determine which booking should be returned. Using the token recovered from the previous step, we were able to request the calendar data associated with another customer’s booking. Bookly returned an ICS response containing the scheduled appointment time, service details and Personally Identifiable Information (PII) information, including the staff member’s name and email address.
For organisations relying on Bookly to manage customer appointments, this creates a meaningful confidentiality impact. An unauthenticated user could use a token obtained from another booking to retrieve information about appointments they were never authorised to access, including PII and details of when and with whom a service was scheduled.
Booking Rollback
The recovered token could also be supplied to the bookly_rollback_order action. This functionality forms part of Bookly’s booking and payment workflow and is used to reverse an Order when the booking process does not complete successfully. The action uses the supplied bookly_order token to determine which Order should be rolled back. Because we had already recovered the token belonging to another customer’s booking, we were able to submit that value and cause Bookly to apply the rollback process to their Order.
Where the associated payment had not been completed, Bookly removed the corresponding customer-appointment record. If no other customers were linked to the appointment, the underlying appointment itself was also permanently deleted. In practice, this meant that an unauthenticated user could move beyond simply viewing information associated with another booking and directly alter its state within the scheduling system.
For organisations relying on Bookly to manage real-world reservations, this could result in unauthorised cancellation of appointments, loss of booking records, disruption to scheduling and service delivery, and potential commercial impact where legitimate reservations are no longer available to fulfil. Because the issue could be exploited against arbitrary valid order IDs, repeated abuse could affect multiple bookings and significantly reduce confidence in the accuracy of the scheduling data held within Bookly. At this point, we were no longer looking at a simple IDOR. What started as control over an internal object reference had opened the door to sensitive booking data, PII and, ultimately, the ability to remove the booking itself. The chain had turned a subtle access-control weakness into a Critical vulnerability.
CVE-2026-93399 Proof of Concept
The following excerpts demonstrate the vulnerability in a local WordPress test environment running a vulnerable version of Bookly. Two independent bookings were used to confirm that an unauthenticated session could be made to reference another customer’s Order, disclose its bookly_order token and subsequently access or remove the associated booking.
Step 1: Create a victim booking
A legitimate booking was first created through Bookly’s public-facing booking form using a configured paid service and the local payment option. Once the booking had been completed, the resulting appointment was confirmed within the WordPress administration interface under Bookly > Calendar. In this example, the victim booking was associated with the following numeric Order identifier:
order_id = 4
This Order was used as the victim booking throughout the remainder of the proof of concept.
Step 2: Create a New Session Referencing the Victim Order
From a separate unauthenticated context, the following request was issued to the bookly_get_form_id AJAX action. The victim’s order_id was supplied within the client-controlled json_data structure:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:1337
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
<SNIP>
action=bookly_get_form_id&json_data={"form_id":"","form_data":{"data":{},"cart":[],"chain":[],"order_id":4,"payment_type":"local"}}
Bookly accepted the supplied Order reference and returned a newly generated form_id:
HTTP/1.1 200 OK
Date: Sun, 05 Jul 2026 15:07:11 GMT
<SNIP>
{
"success": true,
"form_id": "04652***REDACTED***c45a",
"status": {
"booking": "new"
}
}
The returned form_id represented a new Bookly session created independently from the victim booking, but the state associated with that session now referenced the victim’s Order.
Step 3: Retrieve the Victim Order Token
The form_id returned in the previous step was then supplied to the bookly_render_complete action, as shown below:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:1337
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
<SNIP>
action=bookly_render_complete&form_id=04652***REDACTED***c45a
Upon receiving the above request, Bookly processed the session and returned the bookly_order token associated with the victim Order:
HTTP/1.1 200 OK
Date: Sun, 05 Jul 2026 15:07:19 GMT
<SNIP>
{
"success": true,
"bookly_order": "f51a***REDACTED***5480",
"html": "div class=\"bookly-progress-tracker bookly-table\">\n <SNIP> iv>\n <\/div>\n"
}
At this stage, the secret token belonging to the victim booking had been recovered without authentication and without any prior knowledge of the token itself.
Step 4: Retrieve Calendar Information
The recovered bookly_order token was then supplied to the bookly_add_to_calendar action:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:1337
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
<SNIP>
action=bookly_add_to_calendar&bookly_order=f51a***REDACTED***5480&calendar=ics
Bookly returned an ICS calendar entry associated with the victim booking:
HTTP/1.1 200 OK
Date: Sun, 05 Jul 2026 15:07:32 GMT
<SNIP>
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Bookly
CALSCALE:GREGORIAN
BEGIN:VEVENT
UID:2026-07-07 10:15:00-7bf06183@http://localhost:1337
ORGANIZER;CN=Theklis***REDACTED***:mailto:***REDACTED***[email protected]
DTSTAMP:20260707T101500Z
DTSTART:20260707T101500Z
DTEND:20260707T111500Z
SUMMARY:Service
DESCRIPTION:Service\nTheklis
END:VEVENT
END:VCALENDAR
The response disclosed appointment information associated with the victim booking, including the scheduled time and service details, together with Personally Identifiable Information (PII) relating to the staff member, including their name and email address.
Step 5: Roll Back the Victim Booking
Finally, the same bookly_order token was supplied to the bookly_rollback_order action:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:1337
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Connection: close
<SNIP>
action=bookly_rollback_order&bookly_order=f51a***REDACTED***5480
Bookly accepted the supplied token and returned a successful response:
HTTP/1.1 200 OK
Date: Sun, 05 Jul 2026 15:08:02 GMT
<SNIP>
{
"success": true
}
The WordPress administration interface was then refreshed under Bookly > Calendar, where it was confirmed that the victim appointment was no longer present. Where the associated payment had not been completed, the rollback process permanently deleted the victim booking and its associated appointment record.
Impact Summary of CVE-2026-93399
The practical impact of CVE-2026-93399 comes from how little an attacker needs to begin exploiting it. The vulnerable functionality is exposed to unauthenticated visitors, Bookly Order IDs are sequential, and exploitation requires neither an existing WordPress account nor any interaction from the victim. An attacker who can identify a valid Order ID can therefore begin working their way into a booking without first possessing the secret token intended to protect it.
Once that boundary is crossed, the consequences affect more than a single piece of booking metadata. The recovered token provides access to appointment information through Bookly’s calendar functionality, including details of the scheduled service and appointment time together with Personally Identifiable Information (PII), such as the staff member’s name and email address. Information associated with a private booking can therefore be retrieved by somebody with no legitimate relationship to that reservation.
The more serious impact comes from Bookly’s rollback functionality. For non-completed Orders, the same token can be used to permanently remove the associated booking records. This allows an unauthenticated attacker to interfere directly with legitimate reservations, affecting the integrity and availability of the scheduling data on which the organisation relies. At scale, the ability to remove arbitrary bookings could disrupt service delivery, result in lost reservations and create direct operational or commercial impact for businesses that depend on Bookly to manage appointments.
Taken together, these behaviours explain why the issue reached Critical severity. The vulnerability does not stop at exposing an internal identifier or leaking a token: it provides an unauthenticated path from a predictable Order reference to confidential booking information and, ultimately, destructive actions against the booking itself. Wordfence ultimately assigned CVE-2026-93399 a CVSS v3.1 score of 9.1 (Critical).
Disclosure Timeline
- 5 July 2026 – The issue was reported to Wordfence as part of the Wordfence Bug Bounty Program.
- 17 September 2026 – Wordfence completed triage and confirmed the vulnerability as part of the coordinated disclosure process.
- 22 September 2026 – Bookly released version 28.3, which addressed the issue, as documented in the plugin’s public changelog.
- 24 September 2026 – The vulnerability entry was created in the Wordfence Intelligence database as part of the ongoing disclosure process.
- 24 September 2026 – Wordfence confirmed that the vulnerability could be shared publicly.
- 25 September 2026 – CVE-2026-93399 appeared in broader public advisory channels following publication to the CVE List.
Conclusion
CVE-2026-93399 demonstrates how a relatively small weakness in application state can develop into a much more significant access-control issue when that state is trusted elsewhere in a workflow. In this case, an attacker-controlled Order ID could be introduced into a newly created Bookly session and later used to recover the secret token associated with another customer’s Order.
Following that token through Bookly’s frontend functionality exposed the true impact of the issue. What began as control over an internal object reference ultimately provided access to booking information, PII and functionality capable of removing the affected booking altogether. The interesting part of the vulnerability was therefore not any single AJAX action in isolation, but how trust carried between different stages of the booking process and allowed a predictable identifier to reach functionality protected by a separate secret token.
If you are running Bookly, updating to version 28.3 or later is the practical fix. More broadly, the issue reinforces the importance of enforcing ownership and authorisation whenever user-controlled object references are processed, particularly where state is carried between multiple components of an application workflow. If you would like to understand how vulnerabilities like this could affect your own applications, our team can help identify and assess security weaknesses through our web application penetration testing services.