
Proof-of-concept exploit for CVE-2026-64638: reflected XSS in WordPress login chained with DOM clobbering to achieve admin account takeover and remote code execution.
Software: WordPress Core ≤ 7.0.2 (all versions prior to 7.0.3)
CVSS: 8.9 (High)
CWE: CWE-79 — Improper Neutralization of Input During Web Page Generation
Authentication Required: None (Pre-Auth)
User Interaction: Active (admin needs to click 1 link)
Impact: XSS → Account Takeover → Remote Code Execution
WordPress is the most popular content management system in the world, accounting for over 40% of all websites on the internet. Every WordPress site has a login page at /wp-login.php — this is a public endpoint that anyone can access without authentication.
When a user enters an incorrect username, WordPress displays an error message containing the exact username that the user just typed: “The username X is not registered on this site.” The problem lies in the fact that the username value is placed directly into the HTML response without going through any escape function — an attacker simply needs to enter HTML/JavaScript instead of a real username, and the code will be executed in the browser.
This is a Reflected XSS flaw — the payload is contained in the request and reflected back identically by the server in the HTML. What makes it dangerous is that the flaw resides on the login page — a place frequently accessed by admins, where admin session cookies can be stolen.
The research team further discovered that this XSS can be chained with a DOM clobbering vulnerability in WordPress's emoji-loader, allowing JavaScript to be loaded from an external server. From there, an attacker can create a new admin account → install a plugin containing a webshell → execute PHP code on the server. This exploit chain is referred to as XSS2Shell.
| Attribute | Value |
|---|---|
| CVE ID | CVE-2026-64638 |
| CVSS Score | 8.9 (High) |
| Software | WordPress Core ≤ 7.0.2 |
| Authentication | None required (Pre-Auth) |
| User Interaction | Requires 1 click (admin clicks link) |
| Attack Complexity | High |
| Patched | WordPress 7.0.3 (08/06/2026) |
| Reporter | pwn.ai team via HackerOne |
| HackerOne Report | #3877102 |
WordPress loads emoji support on every page (including the login page) via the file emoji-loader.js. This script reads configuration from an element with id="wp-emoji-settings":
// Before patch (vulnerable)
const settings = JSON.parse(
document.getElementById('wp-emoji-settings').textContent
);
document.getElementById() returns the first element in the DOM with a matching id. If an attacker injects a <div id="wp-emoji-settings"> before the original script tag, getElementById will read the attacker's content instead of the real configuration. This technique is called DOM clobbering — overwriting JavaScript behavior by injecting HTML elements.
The emoji configuration contains a URL to load a JavaScript file (concatemoji). The attacker controls this URL → loads a JS file from an external server → executes arbitrary code within the browser context.
Once JavaScript execution within the admin context is achieved, the attacker has full WordPress admin privileges:
/wp-admin/user-new.php with the admin session/wp-admin/plugin-install.phpAny of the 3 methods above allows execution of PHP code on the server — meaning RCE.
From the fix commit 0d6d42e on wordpress-develop, I identified 3 locations in the file wp-includes/user.php where the username/email is placed directly into the error message:
# View diff between vulnerable and patched versions
git diff 7.0.2..7.0.3 -- src/wp-includes/user.php
Location 1 — Line 189 (username does not exist):
Before :
// BEFORE (vulnerable):
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
$username // ← no escaping
)

After :
// fixed:
sprintf(
__( 'The username <strong>%s</strong> is not registered...' ),
esc_html( $username ) // ← escaped
)
Location 2 — Line 216 (wrong password):
Before :

// BEFORE:
'<strong>' . $username . '</strong>' // ← no escaping
After :
// AFTER:
'<strong>' . esc_html( $username ) . '</strong>'
Location 3 — Line 299 (wrong password for email):
Before :

// BEFORE:
'<strong>' . $email . '</strong>' // ← no escaping
After :
// AFTER:
'<strong>' . esc_html( $email ) . '</strong>'
Data flow from the POST request to the error message:
$_POST['log'] ← user input from login form
↓
wp_signon() [user.php:51]
$credentials['user_login'] = wp_unslash($_POST['log']) ← only removes backslashes
↓
wp_authenticate($username, $password) [pluggable.php:689]
$username = sanitize_user($username) ← strips HTML tags, but has a bypass
↓
wp_authenticate_username_password() [user.php:153]
get_user_by('login', $username) ← user not found
↓
sprintf('The username <strong>%s</strong>...', $username) ← XSS!
↓
WP_Error → login_header() → wp_admin_notice()
wp_kses_post(...) ← filters HTML but allows <div>, <a>, through
↓
HTML response → browser render → JavaScript execute
WordPress has 2 filtering layers before the username reaches the HTML:
Layer 1: sanitize_user() — Calls strip_tags() to remove HTML tags. However, PHP's strip_tags() has known limitations: non-standard tag formatting can bypass the filter.
Layer 2: wp_kses_post() — Allows a safe subset of HTML through, including <div>, <a>, `` with certain attributes (but strips event handlers like onerror, onload). Crucially: wp_kses_post allows <div id="wp-emoji-settings"> — precisely the element needed for DOM clobbering.
The pwn.ai team found a way to bypass both layers to inject a useful payload. Specific technical details have not been publicly released.
To visually confirm that the username goes straight into HTML without escaping, I used Xdebug + VS Code to place breakpoints at key points in the execution chain.
Step 1 — Input XSS payload into the login form:
Access http://localhost:8282/wp-login.php, enter the username as `` then click Log In. An alert popup appears — XSS works.


Step 2 — Breakpoint at user.php:184 — Where XSS occurs: