Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- 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