CVE-2025-15370

Shield Security <= 21.0.9 - Authenticated (Subscriber+) Insecure Direct Object Reference to Disable Google Authenticator

mediumAuthorization Bypass Through User-Controlled Key
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
21.0.10
Patched in
1d
Time to patch

Description

The Shield: Blocks Bots, Protects Users, and Prevents Security Breaches plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 21.0.9 via the MfaGoogleAuthToggle class due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Subscriber-level access and above, to disable Google Authenticator for any user.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=21.0.9
PublishedJanuary 15, 2026
Last updatedJanuary 16, 2026
Affected pluginwp-simple-firewall

What Changed in the Fix

Changes introduced in v21.0.10

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2025-15370 ## 1. Vulnerability Summary The **Shield Security** plugin (versions <= 21.0.9) contains an Insecure Direct Object Reference (IDOR) vulnerability in the `MfaGoogleAuthToggle` action. The vulnerability exists because the plugin fails to verify that the u…

Show full research plan

Exploitation Research Plan - CVE-2025-15370

1. Vulnerability Summary

The Shield Security plugin (versions <= 21.0.9) contains an Insecure Direct Object Reference (IDOR) vulnerability in the MfaGoogleAuthToggle action. The vulnerability exists because the plugin fails to verify that the user ID provided in the request matches the currently authenticated user's ID, or that the requester has administrative privileges to modify other users' settings. This allows an authenticated attacker with a low-privileged account (Subscriber+) to disable Google Authenticator (2FA) for any other user, including administrators.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: shield_action (The main router for Shield's internal actions)
  • Sub-Action (Action Slug): mfa_google_auth_toggle
  • Vulnerable Parameter: user_id (within the action_data array)
  • Authentication Level: Subscriber or higher.
  • Preconditions:
    1. The attacker must have a valid login session.
    2. The victim (e.g., Administrator) must have Google Authenticator enabled.
    3. The Shield "Login Guard" module must have Google Authenticator enabled in its settings.

3. Code Flow

  1. Entry Point: The request hits admin-ajax.php with action=shield_action.
  2. Routing: The Shield ActionRouter captures the request. It identifies the target action based on the sub_action parameter (slug: mfa_google_auth_toggle).
  3. Action Execution: The MfaGoogleAuthToggle class (which extends BaseAction) is instantiated.
  4. Access Control: BaseAction::checkAccess() is called.
    • It checks isUserAuthRequired(). Since the capability required for profile updates is likely read or subscriber level, a Subscriber passes this check.
    • It checks for a valid Shield Action Nonce.
  5. Vulnerable Logic: The exec() method of MfaGoogleAuthToggle (inferred) likely retrieves a user_id from the action_data (populated from $_POST).
  6. The Sink: It calls a provider method (likely in FernleafSystems\Wordpress\Plugin\Shield\Modules\LoginGuard\Lib\TwoFactor\Provider\GoogleAuth) to disable or toggle the 2FA state for the specified user_id without verifying if current_user_id == user_id.

4. Nonce Acquisition Strategy

Shield Security localizes its nonces and configuration data into JavaScript objects on specific admin pages.

  1. Page to Visit: The "Your Profile" (/wp-admin/profile.php) or the dedicated "Shield -> Login Security" page.
  2. Shortcode/Trigger: Shield enqueues the userprofile script handle on the profile page.
  3. JS Variable Name: From plugin.json, the handle is userprofile. The localization key is typically shield_vars_userprofile or shield_vars_main.
  4. Extraction Steps:
    • Log in as the Subscriber.
    • Navigate to wp-admin/profile.php.
    • Use browser_eval to extract the nonce:
      // Likely location of the nonce
      window.shield_vars_userprofile?.ajax?.shield_action?.nonce
      // OR specifically for the mfa_google_auth_toggle if distinct
      window.shield_vars_userprofile?.ajax?.mfa_google_auth_toggle?.nonce
      

5. Exploitation Strategy

The goal is to disable Google Authenticator for User ID 1 (Admin) while logged in as User ID 2 (Subscriber).

Step-by-step:

  1. Log in as Subscriber: Obtain a session cookie.
  2. Extract Nonce: Follow the strategy in Section 4 to get a valid shield_action nonce.
  3. Craft the Exploit Request:
    • URL: http://[target]/wp-admin/admin-ajax.php
    • Method: POST
    • Content-Type: application/x-www-form-urlencoded
    • Body Parameters:
      • action: shield_action
      • exec: mfa_google_auth_toggle (Note: Shield often uses exec or sub_action to route the internal action)
      • user_id: 1 (The victim Admin ID)
      • _ajax_nonce: [EXTRACTED_NONCE]
      • state: 0 (To disable) or simply sending the request if it's a hard toggle.

Example Request:

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in_...

action=shield_action&exec=mfa_google_auth_toggle&user_id=1&_ajax_nonce=abcdef1234

6. Test Data Setup

  1. Create Admin User (Victim): User ID 1.
  2. Enable Shield 2FA:
    • Navigate to Shield -> Login Guard -> 2FA.
    • Enable "Google Authenticator".
  3. Activate GA for Admin: Use the Shield profile settings to set up a secret key for the Admin user.
  4. Create Subscriber User (Attacker): User ID 2.
  5. Identify ID: Confirm Admin is 1 and Subscriber is 2 using wp user list.

7. Expected Results

  • The server should respond with a JSON object indicating success (e.g., {"success": true}).
  • The user_meta associated with User 1 regarding Google Authenticator (often key shield_ga_secret or similar in the wp_user_meta table) will be removed or marked as disabled.
  • Upon the next login attempt, the Admin (User 1) will no longer be prompted for a Google Authenticator code.

8. Verification Steps

  1. WP-CLI Check:
    # Check user meta for the admin user
    wp user meta list 1 --keys=shield_ga_secret,shield_ga_validated
    
    If the exploit worked, these meta keys should either be empty, removed, or shield_ga_validated set to 0.
  2. Manual Login: Attempt to log in as the Administrator. If the 2FA screen is bypassed, the exploit is successful.

9. Alternative Approaches

If the mfa_google_auth_toggle slug does not work:

  • Check for mfa_provider_toggle with a provider=ga parameter.
  • Check if the action is handled via a REST API endpoint instead of admin-ajax.php. Shield sometimes registers shield/v1/action/... routes.
  • If the nonce is tied specifically to the action, look for window.shield_vars_userprofile?.ajax?.shield_action specifically.
Research Findings
Static analysis — not yet PoC-verified

Summary

Shield Security (<= 21.0.9) is vulnerable to an Insecure Direct Object Reference (IDOR) via the `mfa_google_auth_toggle` action. This allows authenticated attackers with Subscriber-level permissions or higher to disable Google Authenticator (2FA) for any other user on the site, including administrators, by supplying a target user ID without proper authorization verification.

Vulnerable Code

// src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php

namespace FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Actions;

use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Actions\Traits\AuthNotRequired;
use FernleafSystems\Wordpress\Plugin\Shield\Modules\LoginGuard\Lib\TwoFactor\Provider\Passkey;

class MfaPasskeyAuthenticationStart extends MfaUserConfigBase {

	use AuthNotRequired;

	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 {
			// Insecure pattern: MfaUserConfigBase actions in 21.0.9 did not sufficiently validate 
			// that action_data['user_id'] matched the active authenticated user session.
			$available = self::con()->comps->mfa->getProvidersAvailableToUser( $user );
			/** @var Passkey $provider */
			$provider = $available[ Passkey::ProviderSlug() ] ?? null;
            // ... (truncated)

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-simple-firewall/21.0.9/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php /home/deploy/wp-safety.org/data/plugin-versions/wp-simple-firewall/21.0.10/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-simple-firewall/21.0.9/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php	2025-12-19 15:06:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-simple-firewall/21.0.10/src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php	2026-01-13 13:33:30.000000000 +0000
@@ -2,27 +2,22 @@
 
 namespace FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Actions;
 
-use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Actions\Traits\AuthNotRequired;
+use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Exceptions\ActionException;
 use FernleafSystems\Wordpress\Plugin\Shield\Modules\LoginGuard\Lib\TwoFactor\Provider\Passkey;
 
-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',
+		];
+	}
 }

Exploit Outline

To exploit this vulnerability, an attacker must first obtain an authenticated session with at least Subscriber privileges. Once logged in, the attacker extracts a valid `shield_action` nonce, which is typically found localized in the `window.shield_vars_userprofile` JavaScript object on the 'Your Profile' page. The attacker then crafts a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `shield_action`, the `exec` parameter set to `mfa_google_auth_toggle`, and the `user_id` parameter set to the target user's ID (e.g., '1' for the primary administrator). Because version 21.0.9 fails to verify that the `user_id` in the request data matches the ID of the currently logged-in user, the plugin will disable Google Authenticator for the victim account, allowing the attacker to bypass 2FA in subsequent login attempts.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.