Campaign Monitor for WordPress <= 2.9.1 - Missing Authorization
Description
The Campaign Monitor for WordPress plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 2.9.1. This makes it possible for authenticated attackers, with subscriber-level access and above, to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.9.1What Changed in the Fix
Changes introduced in v2.9.2
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-0674 - Missing Authorization in Campaign Monitor for WordPress ## 1. Vulnerability Summary The **Campaign Monitor for WordPress** plugin (up to version 2.9.1) contains a missing authorization vulnerability in its OAuth authentication handler. The function `for…
Show full research plan
Exploitation Research Plan: CVE-2026-0674 - Missing Authorization in Campaign Monitor for WordPress
1. Vulnerability Summary
The Campaign Monitor for WordPress plugin (up to version 2.9.1) contains a missing authorization vulnerability in its OAuth authentication handler. The function forms\core\Application::authenticate() processes raw JSON input from php://input to update the plugin's client_id and client_secret settings. This process lacks capability checks (e.g., current_user_can('manage_options')) and nonce verification. Consequently, any authenticated user with subscriber-level access can overwrite the plugin's API credentials, potentially hijacking the Campaign Monitor integration.
2. Attack Vector Analysis
- Endpoint: Any administrative URL (e.g.,
/wp-admin/admin-ajax.phpor/wp-admin/index.php) because the handler is likely triggered via a hook likeplugins_loadedorinitwithinforms\core\Application::run(). - HTTP Method:
POST - Authentication: Authenticated (Subscriber level or higher).
- Payload: A JSON object in the request body containing
ClientIdandClientSecret. - Preconditions: The plugin must be active.
3. Code Flow
- Entry: The main plugin file
campaign-monitor.phpregisters a hook:add_action('plugins_loaded', function(){ \forms\core\Application::run(); });. - Initialization:
Application::run()is invoked. While the full code ofrun()is truncated, it typically callsself::authenticate()or registers it to an early hook likeinit. - Vulnerable Sink:
Application::authenticate()(informs/core/Application.php) executes:- It reads the request body:
$fileContent = file_get_contents("php://input");. - It decodes the JSON:
$credentials = json_decode($fileContent);. - If
ClientIdandClientSecretare present, it updates settings:Settings::add('client_secret', $clientSecret ); Settings::add('client_id', $clientId);
- It reads the request body:
- Result: The plugin's API configuration is changed. The function then attempts to redirect to the Campaign Monitor authorize URL:
\wp_redirect($authorizeUrl); exit();.
4. Nonce Acquisition Strategy
This vulnerability bypasses the nonce system entirely because the vulnerable code path reads directly from php://input and does not invoke check_ajax_referer() or wp_verify_nonce(). No nonce is required for exploitation.
5. Exploitation Strategy
The goal is to demonstrate that a subscriber can change the plugin's client_id.
Step-by-Step Plan:
- Login: Authenticate as a Subscriber user to obtain a session cookie.
- Craft Payload: Create a JSON body with arbitrary credentials.
{ "ClientId": "pwned_client_id", "ClientSecret": "pwned_client_secret" } - Send Request: Use
http_requestto send a POST request to/wp-admin/with the JSON payload and the subscriber cookie. - Observe Response: A successful exploit will trigger a
302 Foundredirect tohttps://api.createsend.com/oauth/(the Campaign Monitor OAuth endpoint).
HTTP Request Details:
- URL:
{{WP_URL}}/wp-admin/ - Method:
POST - Headers:
Content-Type: application/jsonCookie: {{Subscriber_Cookie}}
- Body:
{"ClientId": "vulnerable_client_id", "ClientSecret": "vulnerable_client_secret"}
6. Test Data Setup
- Install Plugin: Ensure "Campaign Monitor for WordPress" v2.9.1 is installed and active.
- Create User:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password123 - Initial State Check: Record current settings (if any).
wp option get campaign_monitor_settings(Note: exact option name may vary; checkforms/core/Settings.phplogic if available, or list all options containing 'campaign').
7. Expected Results
- HTTP Response: The server should respond with a
302redirect. - Location Header: The
Locationheader in the response will point tohttps://api.createsend.com/oauth/...and include theclient_id=vulnerable_client_id. - Database Change: The plugin's stored options for
client_idandclient_secretwill be updated to the values provided in the JSON payload.
8. Verification Steps
After sending the HTTP request, verify the settings change via WP-CLI:
# Search for options modified by the plugin
wp option list --search="*campaign*"
# Specifically check the client_id value
# Assuming the setting class uses the 'cm_forms_settings' or similar option key
wp option get cm_forms_settings
9. Alternative Approaches
If sending the payload to /wp-admin/ does not trigger the handler:
- Target AJAX: Send the same JSON payload to
/wp-admin/admin-ajax.php. Even if noactionparameter is provided,plugins_loadedandinithooks still fire. - Error Trigger: Some parts of
authenticate()check for$_GET['error']. Try adding?page=campaign-monitor-for-wordpress&error=testto the URL while sending the JSON body to ensure the logic path is reached. - Check for manual trigger: The
Ajax::run()method shows a manual trigger:
If theif (!empty($_POST['action']) && $_POST['action'] === 'ajax_handler_nopriv_cm_forms'){ self::ajaxFormHandler(); }authenticatelogic is insideajaxFormHandleror its dependencies, sendaction=ajax_handler_nopriv_cm_formsas a query parameter while providing the JSON body.
Summary
The Campaign Monitor for WordPress plugin through 2.9.1 lacks authorization and nonce checks in its OAuth authentication handler. This allows authenticated attackers with subscriber-level access to overwrite the plugin's Campaign Monitor Client ID and Client Secret by sending a crafted JSON payload to an administrative endpoint.
Vulnerable Code
// forms/core/Application.php (around line 103) public static function authenticate() { // ... [Truncated error handling logic] ... $fileContent = file_get_contents("php://input"); if (!empty($fileContent)){ $credentials = json_decode($fileContent); if (!empty($credentials) && (isset($credentials->ClientId) && isset($credentials->ClientSecret))){ // extract client id and client secret from post request $clientId = $credentials->ClientId; $clientSecret = $credentials->ClientSecret; // save for subsequent request Settings::add('client_secret', $clientSecret ); Settings::add('client_id', $clientId); $authorizeUrl = self::$CampaignMonitor->authorize_url($clientId,Helper::getRedirectUrl() , Helper::getCampaignMonitorPermissions() ); Log::write( "Authorizing: " . $authorizeUrl ); // redirect to get an access token \wp_redirect($authorizeUrl); exit(); } }
Security Fix
@@ -5,7 +5,7 @@ * Plugin Name: Campaign Monitor for WordPress * Plugin URI: http://campaignmonitor.com * Description: Manage Campaign Monitor Lists, Custom Fields and add forms and how you show them to your users.. - * Version: 2.9.1 + * Version: 2.9.2 * Author: Campaign Monitor * Author URI: http://campaignmonitor.com * License: License: GPLv2 or later @@ -44,6 +44,8 @@ wp_die( 'Unauthorized', '', array( 'response' => 403 ) ); } + check_ajax_referer( 'cm_forms_ajax', 'nonce' ); + Application::refreshTokenIfNeeded(); // we could further optimize the plugin with one entry point for all ajax requests @@ -14,7 +14,7 @@ */ public static $CampaignMonitor = null; - const VERSION = '2.9.1'; + const VERSION = '2.9.2'; public static $shortCodeId = ''; /** @@ -971,7 +971,8 @@ // in JavaScript, object properties are accessed as ajax_object.ajax_url, ajax_object.we_value wp_localize_script(Helper::tokenize('ajax-script'), 'ajax_request', array( - 'ajax_url' => admin_url('admin-ajax.php') + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('cm_forms_ajax'), )); } }
Exploit Outline
The exploit targets the `forms\core\Application::authenticate()` function which is triggered during plugin initialization on any `/wp-admin/` request. An attacker authenticates as a low-privileged user (e.g., Subscriber) and sends a POST request to `/wp-admin/` with a JSON body containing malicious `ClientId` and `ClientSecret` values. Because the function reads directly from `php://input` without verifying user capabilities or checking for nonces, it updates the site's Campaign Monitor configuration with the attacker's values. Successful exploitation is confirmed by a 302 redirect to the Campaign Monitor authorization URL containing the attacker-supplied Client ID.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.