Shield Security <= 21.0.9 - Authenticated (Subscriber+) Insecure Direct Object Reference to Disable Google Authenticator
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:NTechnical Details
<=21.0.9What Changed in the Fix
Changes introduced in v21.0.10
Source Code
WordPress.org SVN# 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 theaction_dataarray) - Authentication Level: Subscriber or higher.
- Preconditions:
- The attacker must have a valid login session.
- The victim (e.g., Administrator) must have Google Authenticator enabled.
- The Shield "Login Guard" module must have Google Authenticator enabled in its settings.
3. Code Flow
- Entry Point: The request hits
admin-ajax.phpwithaction=shield_action. - Routing: The Shield
ActionRoutercaptures the request. It identifies the target action based on thesub_actionparameter (slug:mfa_google_auth_toggle). - Action Execution: The
MfaGoogleAuthToggleclass (which extendsBaseAction) is instantiated. - Access Control:
BaseAction::checkAccess()is called.- It checks
isUserAuthRequired(). Since the capability required for profile updates is likelyreadorsubscriberlevel, a Subscriber passes this check. - It checks for a valid Shield Action Nonce.
- It checks
- Vulnerable Logic: The
exec()method ofMfaGoogleAuthToggle(inferred) likely retrieves auser_idfrom theaction_data(populated from$_POST). - 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 specifieduser_idwithout verifying ifcurrent_user_id == user_id.
4. Nonce Acquisition Strategy
Shield Security localizes its nonces and configuration data into JavaScript objects on specific admin pages.
- Page to Visit: The "Your Profile" (
/wp-admin/profile.php) or the dedicated "Shield -> Login Security" page. - Shortcode/Trigger: Shield enqueues the
userprofilescript handle on the profile page. - JS Variable Name: From
plugin.json, the handle isuserprofile. The localization key is typicallyshield_vars_userprofileorshield_vars_main. - Extraction Steps:
- Log in as the Subscriber.
- Navigate to
wp-admin/profile.php. - Use
browser_evalto 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:
- Log in as Subscriber: Obtain a session cookie.
- Extract Nonce: Follow the strategy in Section 4 to get a valid
shield_actionnonce. - 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_actionexec:mfa_google_auth_toggle(Note: Shield often usesexecorsub_actionto 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.
- URL:
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
- Create Admin User (Victim): User ID 1.
- Enable Shield 2FA:
- Navigate to Shield -> Login Guard -> 2FA.
- Enable "Google Authenticator".
- Activate GA for Admin: Use the Shield profile settings to set up a secret key for the Admin user.
- Create Subscriber User (Attacker): User ID 2.
- Identify ID: Confirm Admin is
1and Subscriber is2usingwp user list.
7. Expected Results
- The server should respond with a JSON object indicating success (e.g.,
{"success": true}). - The
user_metaassociated with User 1 regarding Google Authenticator (often keyshield_ga_secretor similar in thewp_user_metatable) 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
- WP-CLI Check:
If the exploit worked, these meta keys should either be empty, removed, or# Check user meta for the admin user wp user meta list 1 --keys=shield_ga_secret,shield_ga_validatedshield_ga_validatedset to0. - 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_togglewith aprovider=gaparameter. - Check if the action is handled via a
REST APIendpoint instead ofadmin-ajax.php. Shield sometimes registersshield/v1/action/...routes. - If the nonce is tied specifically to the action, look for
window.shield_vars_userprofile?.ajax?.shield_actionspecifically.
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
@@ -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.