CVE-2026-0674

Campaign Monitor for WordPress <= 2.9.1 - Missing Authorization

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
2.9.2
Patched in
124d
Time to patch

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: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<=2.9.1
PublishedJanuary 8, 2026
Last updatedMay 11, 2026

What Changed in the Fix

Changes introduced in v2.9.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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.php or /wp-admin/index.php) because the handler is likely triggered via a hook like plugins_loaded or init within forms\core\Application::run().
  • HTTP Method: POST
  • Authentication: Authenticated (Subscriber level or higher).
  • Payload: A JSON object in the request body containing ClientId and ClientSecret.
  • Preconditions: The plugin must be active.

3. Code Flow

  1. Entry: The main plugin file campaign-monitor.php registers a hook:
    add_action('plugins_loaded', function(){ \forms\core\Application::run(); });.
  2. Initialization: Application::run() is invoked. While the full code of run() is truncated, it typically calls self::authenticate() or registers it to an early hook like init.
  3. Vulnerable Sink: Application::authenticate() (in forms/core/Application.php) executes:
    • It reads the request body: $fileContent = file_get_contents("php://input");.
    • It decodes the JSON: $credentials = json_decode($fileContent);.
    • If ClientId and ClientSecret are present, it updates settings:
      Settings::add('client_secret', $clientSecret );
      Settings::add('client_id', $clientId);
      
  4. 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:

  1. Login: Authenticate as a Subscriber user to obtain a session cookie.
  2. Craft Payload: Create a JSON body with arbitrary credentials.
    {
      "ClientId": "pwned_client_id",
      "ClientSecret": "pwned_client_secret"
    }
    
  3. Send Request: Use http_request to send a POST request to /wp-admin/ with the JSON payload and the subscriber cookie.
  4. Observe Response: A successful exploit will trigger a 302 Found redirect to https://api.createsend.com/oauth/ (the Campaign Monitor OAuth endpoint).

HTTP Request Details:

  • URL: {{WP_URL}}/wp-admin/
  • Method: POST
  • Headers:
    • Content-Type: application/json
    • Cookie: {{Subscriber_Cookie}}
  • Body: {"ClientId": "vulnerable_client_id", "ClientSecret": "vulnerable_client_secret"}

6. Test Data Setup

  1. Install Plugin: Ensure "Campaign Monitor for WordPress" v2.9.1 is installed and active.
  2. Create User:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password123
  3. Initial State Check: Record current settings (if any).
    wp option get campaign_monitor_settings (Note: exact option name may vary; check forms/core/Settings.php logic if available, or list all options containing 'campaign').

7. Expected Results

  • HTTP Response: The server should respond with a 302 redirect.
  • Location Header: The Location header in the response will point to https://api.createsend.com/oauth/... and include the client_id=vulnerable_client_id.
  • Database Change: The plugin's stored options for client_id and client_secret will 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:

  1. Target AJAX: Send the same JSON payload to /wp-admin/admin-ajax.php. Even if no action parameter is provided, plugins_loaded and init hooks still fire.
  2. Error Trigger: Some parts of authenticate() check for $_GET['error']. Try adding ?page=campaign-monitor-for-wordpress&error=test to the URL while sending the JSON body to ensure the logic path is reached.
  3. Check for manual trigger: The Ajax::run() method shows a manual trigger:
    if (!empty($_POST['action']) && $_POST['action'] === 'ajax_handler_nopriv_cm_forms'){
        self::ajaxFormHandler();
    }
    
    If the authenticate logic is inside ajaxFormHandler or its dependencies, send action=ajax_handler_nopriv_cm_forms as a query parameter while providing the JSON body.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.1/campaign-monitor.php /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.2/campaign-monitor.php
--- /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.1/campaign-monitor.php	2026-04-16 01:55:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.2/campaign-monitor.php	2026-05-05 04:32:20.000000000 +0000
@@ -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
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.1/forms/core/Ajax.php /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.2/forms/core/Ajax.php
--- /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.1/forms/core/Ajax.php	2026-04-16 01:55:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.2/forms/core/Ajax.php	2026-05-05 04:32:20.000000000 +0000
@@ -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
 		
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.1/forms/core/Application.php /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.2/forms/core/Application.php
--- /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.1/forms/core/Application.php	2026-04-16 01:55:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/forms-for-campaign-monitor/2.9.2/forms/core/Application.php	2026-05-05 04:32:20.000000000 +0000
@@ -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.