ManageWP Worker <= 4.9.31 - Unauthenticated Stored Cross-Site Scripting via 'MWP-Key-Name' Header
Description
The ManageWP Worker plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'MWP-Key-Name' HTTP request header in all versions up to, and including, 4.9.31. This is due to insufficient input sanitization and output escaping of attacker-controlled header values. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever an administrator visits the plugin's connection management page with debug parameters.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v4.9.32
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-3718 (ManageWP Worker Stored XSS) ## 1. Vulnerability Summary The ManageWP Worker plugin (versions <= 4.9.31) is vulnerable to unauthenticated stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin logs failed communication attempts, in…
Show full research plan
Exploitation Research Plan: CVE-2026-3718 (ManageWP Worker Stored XSS)
1. Vulnerability Summary
The ManageWP Worker plugin (versions <= 4.9.31) is vulnerable to unauthenticated stored Cross-Site Scripting (XSS). The vulnerability exists because the plugin logs failed communication attempts, including the attacker-controlled MWP-Key-Name HTTP header, into a WordPress option (mwp_last_communication_error) without sufficient sanitization. When an administrator views the plugin's connection management page with debug parameters enabled, this stored payload is echoed into the DOM, leading to script execution in the context of the administrator's session.
2. Attack Vector Analysis
- Target Endpoint: Any URL handled by WordPress (e.g.,
/index.phpor/wp-admin/admin-ajax.php). - Vulnerable Header:
MWP-Key-Name - Required Header for Activation:
MWP-Action(to trigger the ManageWP request lifecycle). - Authentication Level: Unauthenticated. No valid credentials or ManageWP secret keys are required to trigger the error logging.
- Preconditions: The ManageWP Worker plugin must be active.
3. Code Flow
- Entry Point: An unauthenticated request is sent containing the
MWP-ActionandMWP-Key-Nameheaders. - Event Trigger: The plugin identifies a ManageWP-style request (via
HTTP_MWP_ACTION) and dispatches theMWP_Event_Events::MASTER_REQUESTevent. - Authentication Listener: The listener
MWP_EventListener_MasterRequest_AuthenticateServiceRequest::onMasterRequest(insrc/MWP/EventListener/MasterRequest/AuthenticateServiceRequest.php) intercepts the request. - Header Extraction: At line 74, the code calls
$keyName = $request->getKeyName();, which extracts the value of theMWP-Key-Nameheader. - Logging (The Sink): Since the request lacks a valid
serviceSignature, the code reaches line 77 or 83.- Line 77:
if (empty($serviceSignature) || empty($keyName)) { $this->context->optionSet('mwp_last_communication_error', '... Key name: '.$keyName.', ...'); } - Line 83: If a signature is provided but the key is not found:
$this->context->optionSet('mwp_last_communication_error', '... Searched for: '.$keyName);
- Line 77:
- Persistence: The
optionSetmethod (a wrapper forupdate_option) stores the unsanitized$keyNamein themwp_last_communication_erroroption. - Execution: When an admin visits
wp-admin/plugins.php?worker_connections=1&mwp_debug=1, the plugin retrieves themwp_last_communication_erroroption and echoes it directly into the page to assist in debugging connection issues.
4. Nonce Acquisition Strategy
No nonce is required for the injection phase.
The vulnerability occurs during the authentication process itself. The error logging mechanism is designed to capture details about failed requests (which inherently cannot have valid nonces or signatures). Therefore, an unauthenticated attacker can populate the mwp_last_communication_error option by simply sending a malformed request.
5. Exploitation Strategy
Step 1: Inject the XSS Payload
Send a request to the WordPress root to trigger the error log entry.
- Tool:
http_request - Method: POST (or GET)
- URL:
/ - Headers:
MWP-Action: versionMWP-Key-Name: <img src=x onerror="alert('CVE-2026-3718')">
- Expected Response: The request will likely return a
200 OKor a ManageWP-specific error, but the payload will be stored in the database.
Step 2: Trigger the Payload (Admin Interaction)
An administrator must navigate to the worker connection management page with debug mode active.
- Target URL:
/wp-admin/plugins.php?worker_connections=1&mwp_debug=1 - Requirement: Must be logged in as a user with
activate_pluginsormanage_optionscapabilities.
6. Test Data Setup
- Install and activate ManageWP Worker 4.9.31.
- No specific ManageWP connection is required; the vulnerability triggers on the "failed to connect" path.
- Ensure the
mwp_last_communication_erroroption is empty or does not exist initially (optional, but good for clean testing).
7. Expected Results
- The
update_optioncall will save the string containing the HTML payload to the database. - Upon visiting the connection management page with
mwp_debug=1, the page source will contain the raw HTML:... Searched for: <img src=x onerror="..."> .... - The browser will execute the JavaScript in the
onerrorattribute.
8. Verification Steps
After sending the injection request via http_request, verify the state of the database using WP-CLI:
# Check if the option contains the payload
wp option get mwp_last_communication_error
The output should look like:Unexpected: service signature or key name are empty. Key name: <img src=x onerror="alert('CVE-2026-3718')">, ...
9. Alternative Approaches
If mwp_debug=1 does not immediately show the error, try the following debug parameters:
debug=1mwp_verbose=1mwp_error_view=1
Additionally, check if the error is displayed as a WordPress Admin Notice (admin_notices hook) on the main plugins page after a failed ManageWP communication attempt. Some versions of the worker plugin display the mwp_last_communication_error as a persistent notice until cleared.
Summary
The ManageWP Worker plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting (XSS) via the 'MWP-Key-Name' HTTP header. Malicious header values are logged into the 'mwp_last_communication_error' option without sanitization and subsequently executed in the browser of an administrator viewing the connection management page.
Vulnerable Code
// src/MWP/EventListener/MasterRequest/AuthenticateServiceRequest.php:71 $keyName = $request->getKeyName(); if (empty($serviceSignature) || empty($keyName)) { $this->context->optionSet('mwp_last_communication_error', 'Unexpected: service signature or key name are empty. Key name: '.$keyName.', Signature: '.$serviceSignature.', Algorithm: '.($algorithm ? $algorithm : 'SHA1')); return; } --- // src/MWP/EventListener/PublicRequest/AddConnectionKeyInfo.php:415 <p> <?php echo 'Last communication error: '.$this->context->optionGet('mwp_last_communication_error', '') ?> </p>
Security Fix
@@ -3,7 +3,7 @@ Plugin Name: ManageWP - Worker Plugin URI: https://managewp.com Description: We help you efficiently manage all your WordPress websites. <strong>Updates, backups, 1-click login, migrations, security</strong> and more, on one dashboard. This service comes in two versions: standalone <a href="https://managewp.com">ManageWP</a> service that focuses on website management, and <a href="https://godaddy.com/pro">GoDaddy Pro</a> that includes additional tools for hosting, client management, lead generation, and more. -Version: 4.9.31 +Version: 4.9.32 Author: GoDaddy Author URI: https://godaddy.com License: GPL2 @@ -575,8 +575,8 @@ // reason (eg. the site can't ping itself). Handle that case early. register_activation_hook(__FILE__, 'mwp_activation_hook'); - $GLOBALS['MMB_WORKER_VERSION'] = '4.9.31'; - $GLOBALS['MMB_WORKER_REVISION'] = '2026-03-10 00:00:00'; + $GLOBALS['MMB_WORKER_VERSION'] = '4.9.32'; + $GLOBALS['MMB_WORKER_REVISION'] = '2026-03-18 00:00:00'; // Ensure PHP version compatibility. if (version_compare(PHP_VERSION, '5.2', '<')) { @@ -46,6 +46,8 @@ } $algorithm = $request->getSignatureAlgorithm(); + // Sanitize algorithm to prevent XSS when displayed in debug output + $sanitizedAlgorithm = sanitize_text_field($algorithm); if ($algorithm == 'SHA256') { $serviceSignature = $request->getServiceSignatureV2(); @@ -56,9 +58,11 @@ } $keyName = $request->getKeyName(); + // Sanitize key name to prevent XSS when displayed in debug output + $sanitizedKeyName = sanitize_text_field($keyName); if (empty($serviceSignature) || empty($keyName)) { - $this->context->optionSet('mwp_last_communication_error', 'Unexpected: service signature or key name are empty. Key name: '.$keyName.', Signature: '.$serviceSignature.', Algorithm: '.($algorithm ? $algorithm : 'SHA1')); + $this->context->optionSet('mwp_last_communication_error', 'Unexpected: service signature or key name are empty. Key name: '.$sanitizedKeyName.', Signature: '.$serviceSignature.', Algorithm: '.($sanitizedAlgorithm ? $sanitizedAlgorithm : 'SHA1')); return; } @@ -67,7 +71,7 @@ if (empty($publicKey)) { // for now do not throw an exception, just do not authenticate the request // later we should start throwing an exception here when this becomes the main communication method - $this->context->optionSet('mwp_last_communication_error', 'Could not find the appropriate communication key. Searched for: '.$keyName); + $this->context->optionSet('mwp_last_communication_error', 'Could not find the appropriate communication key. Searched for: '.$sanitizedKeyName); return; } @@ -75,7 +79,7 @@ $messageToCheck = ''; if (empty($communicationKey)) { - $this->context->optionSet('mwp_last_communication_error', 'Unexpected: communication key is empty. Key name: '.$keyName); + $this->context->optionSet('mwp_last_communication_error', 'Unexpected: communication key is empty. Key name: '.$sanitizedKeyName); return; } @@ -88,7 +92,7 @@ if (empty($messageToCheck)) { // for now do not throw an exception, just do not authenticate the request // later we should start throwing an exception here when this becomes the main communication method - $this->context->optionSet('mwp_last_communication_error', 'Unexpected: message to check is empty. Host: '.$request->server['HTTP_HOST']); + $this->context->optionSet('mwp_last_communication_error', 'Unexpected: message to check is empty. Host: '.sanitize_text_field($request->server['HTTP_HOST'])); return; } @@ -410,21 +410,28 @@ if ($refreshedKeys['success'] === true) { echo 'Keys successfully refreshed!'; } else { - echo 'Keys were not successfully refreshed. Error: '.$refreshedKeys['message']; + echo 'Keys were not successfully refreshed. Error: '.esc_html($refreshedKeys['message']); } ?> </p> <p> - <?php echo 'Last communication error: '.$this->context->optionGet('mwp_last_communication_error', '') ?> + <?php echo 'Last communication error: '.esc_html($this->context->optionGet('mwp_last_communication_error', '')) ?> </p>
Exploit Outline
An unauthenticated attacker sends a crafted HTTP request to any WordPress endpoint. The request must include the 'MWP-Action' header (e.g., set to 'version') to trigger the plugin's ManageWP event dispatcher and the 'MWP-Key-Name' header containing an XSS payload (e.g., <img src=x onerror=alert(1)>). Because the request is unauthenticated, it reaches an error-handling block that stores the unsanitized 'MWP-Key-Name' value into the 'mwp_last_communication_error' option via WordPress's update_option. The payload is triggered when a logged-in administrator visits the worker connection management page (wp-admin/plugins.php?worker_connections=1) with debug parameters (mwp_debug=1) enabled.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.