Shield Security <= 21.0.8 - Unauthenticated Reflected Cross-Site Scripting via 'message' Parameter
Description
The Shield Security plugin for WordPress is vulnerable to Reflected Cross-Site Scripting via the 'message' parameter in all versions up to, and including, 21.0.8 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user into performing an action such as clicking on a link.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:NTechnical Details
<=21.0.8What Changed in the Fix
Changes introduced in v21.0.10
Source Code
WordPress.org SVNThis research plan targets a reflected Cross-Site Scripting (XSS) vulnerability in the Shield Security plugin. The vulnerability arises from the unescaped reflection of the `message` query parameter on pages rendered via the plugin's `ActionRouter` system. ### 1. Vulnerability Summary - **Vulnerabi…
Show full research plan
This research plan targets a reflected Cross-Site Scripting (XSS) vulnerability in the Shield Security plugin. The vulnerability arises from the unescaped reflection of the message query parameter on pages rendered via the plugin's ActionRouter system.
1. Vulnerability Summary
- Vulnerability: Unauthenticated Reflected Cross-Site Scripting (XSS).
- Component:
ActionRoutersystem. - Variable:
messageparameter. - Cause: The plugin's action processing logic accepts a
messageparameter from the request (GET or POST) and echoes it back in the generated HTML response (such as a block page, login page, or error notice) without sufficient sanitization or output escaping. - Affected Versions: <= 21.0.8.
2. Attack Vector Analysis
- Endpoint: Any URL where the Shield
ActionRouteris invoked. This is typically the main site index with ashield_actionparameter orwp-login.php. - Parameter:
message(GET parameter). - Authentication: None (Unauthenticated).
- Bypass: Shield's
BaseAction.phpcontains a logic flaw where nonces are only verified for AJAX requests. A standard browserGETrequest bypasses nonce verification. - Payload: A standard
<script>or event handler payload, e.g.,<script>alert(document.domain)</script>.
3. Code Flow
- Entry Point: A user navigates to
/?shield_action=[ACTION_SLUG]&message=[PAYLOAD]. - Action Initialization:
BaseAction::__construct()merges request data into$this->action_data. Themessageparameter from the URL is now in$this->action_data['message']. - Access Check:
BaseAction::process()callscheckAccess(). - Nonce Bypass: In
BaseAction::isNonceVerifyRequired(), the code checksself::con()->this_req->wp_is_ajax. For a direct browserGETrequest, this isfalse, andisNonceVerifyRequired()returnsfalse, bypassingActionNonce::VerifyFromRequest(). - Action Execution: If the action (e.g.,
mfa_passkey_auth_start) uses theAuthNotRequiredtrait, it proceeds without a logged-in session. - Sink: The action completes or throws an exception. The
ActionRouterthen renders a response page. The template logic (not provided, but inferred) retrieves themessagefrom the request or theaction_dataand echoes it directly into the HTML to display a notice to the user.
4. Nonce Acquisition Strategy
According to src/lib/src/ActionRouter/Actions/BaseAction.php, nonces are not required for non-AJAX requests:
protected function isNonceVerifyRequired() :bool {
return (bool)( $this->getActionOverrides()[ Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED ] ?? self::con()->this_req->wp_is_ajax );
}
If wp_is_ajax is false, the check is skipped. This allows unauthenticated exploitation via a direct link.
If a specific action does override this and require a nonce, it can be found in the browser context:
- Action Slug:
mfa_passkey_auth_start(confirmed in source). - JS Variable:
window.shield_vars_login_2faorwindow.shield_vars_main. - Extraction:
browser_eval("window.shield_vars_main.ajax.shield_action.nonce")(inferred path).
5. Exploitation Strategy
We will use the `mfa_passkey_auth_start
Summary
The Shield Security plugin for WordPress is vulnerable to unauthenticated reflected Cross-Site Scripting (XSS) via the 'message' parameter. This occurs because the plugin's ActionRouter system echoes the 'message' query parameter directly into the HTML response of various actions without sufficient sanitization or output escaping.
Vulnerable Code
// src/lib/src/ActionRouter/Actions/BaseAction.php public function __construct( array $data = [], ?ActionResponse $response = null ) { $this->action_data = $data; // Request data (GET/POST) is merged directly into action_data $this->response = $response instanceof ActionResponse ? $response : new ActionResponse(); } // ... protected function isNonceVerifyRequired() :bool { // Nonce verification is skipped for non-AJAX (direct GET) requests by default return (bool)( $this->getActionOverrides()[ Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED ] ?? self::con()->this_req->wp_is_ajax ); } --- // src/lib/src/ActionRouter/Actions/MfaPasskeyAuthenticationStart.php class MfaPasskeyAuthenticationStart extends MfaUserConfigBase { use AuthNotRequired; // Allows unauthenticated users to trigger this action public const SLUG = 'mfa_passkey_auth_start'; protected function exec() { // ... reflection of messages in response data often happens via the 'message' key
Security Fix
@@ -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; } @@ -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
The exploit targets the Shield ActionRouter system. An attacker crafts a GET request to the site's index or login page using the `shield_action` parameter to trigger a specific action that does not require authentication or nonce verification (e.g., `mfa_passkey_auth_start`). By including a malicious payload in the `message` parameter (e.g., `/?shield_action=mfa_passkey_auth_start&message=<script>alert(document.domain)</script>`), the attacker triggers the ActionRouter logic which reflects the `message` input directly into the rendered HTML response page. Since the plugin fails to sanitize or escape this value before outputting it, the script executes in the victim's browser context. Authentication is not required if a publicly accessible action is used.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.