WordPress 7.0.3 patches pre-auth login XSS to RCE (CVE-2026-64638, XSS2Shell)
Contents
TL;DR
What happened Aug 6, 2026 — WordPress shipped security release 7.0.3. The headline fix is a pre-auth reflected XSS on the login screen (CVE-2026-64638 / GHSA-52p2-r8wf-jcrf, aka XSS2Shell, CVSS 8.9).
What’s affected Every WordPress version is vulnerable. The fix ships in 7.0.3 and is backported to each maintained security branch down to 4.7; 7.1 RC2 also carries it. Treat effectively all live installs as in scope.
What’s reachable without auth The XSS at the top of the chain needs no account. Reaching PHP execution, though, takes social engineering: a logged-in admin has to be lured to the attacker’s link and click.
Why it triggers When wp_strip_all_tags() handles a nonexistent username, a < area with a space after < is read as harmless text by PHP’s strip_tags(), but the later KSES pass restores the same string as a real HTML tag. That disagreement leaves the tag in the page.
Who found it pwn.ai found it autonomously with open-source models and a multi-agent workflow — chain reproduced July 26, reported July 27. No in-the-wild exploitation as of August 7.
What to do Update to 7.0.3, or the backport for your branch, now. Apply it from Dashboard → Updates.
WordPress shipped security release 7.0.3 on August 6, 2026.
The release bundles 12 vulnerability fixes. The one officially marked pre-auth is a reflected XSS on the login screen (CVE-2026-64638), and under the right conditions it chains all the way to PHP code execution. pwn.ai, who found it, named the chain “XSS2Shell.”
WordPress runs more than 40% of the web, and in a stock setup the login screen (wp-login.php) is public. The weak spot sits in that login screen, in its pre-authentication input handling, so essentially every install exposes it.
What reflected XSS is
Reflected XSS gets a victim to follow a link carrying attacker script, then runs that script in the victim’s browser with the privileges of the visited site. It works wherever a submitted value is echoed straight back into the page (reflected). Here, the error message shown on a failed login echoes the username you entered.
“Reachable without auth” means the attacker needs no login account. Anyone can trigger the error message, so getting the XSS to fire has almost no preconditions.
strip_tags misses the whitespace tag, KSES picks it up
When WordPress receives a nonexistent username, it builds the error message through wp_strip_all_tags(). That function calls PHP’s strip_tags() internally to remove HTML tags. But a later stage runs a separate sanitizer, wp_kses_post() (known as KSES), which handles allowed HTML in post content and elsewhere.
The problem is that the two disagree on whether a space right after < starts a tag. Given a string like < area with a space after <, strip_tags() decides it is “not a tag, just text” and lets it through. KSES then re-reads the same < area as a proper <area> element. Per pwn.ai, strip_tags('< area id=test>') returns the string intact, while the space-free strip_tags('<area id=test>') gets stripped as a tag.
Exploiting that gap lets an attacker push live elements that should have been removed — <area>, <div>, <button> — into the login page without any authentication.
From one click to PHP execution
From there the injected tag chains through several techniques to reach PHP execution. The flow looks like this.
flowchart TD
A[Unauth: inject whitespace tag into login page] --> B[DOM clobbering<br/>overwrite ajaxurl with attacker element]
B --> C[JSONP call to REST API<br/>_jsonp=alert turns response into JS]
C --> D[Same Origin Method Execution<br/>invoke admin actions across windows]
D --> E[Steal application password]
E --> F[Publish malicious page via unfiltered_html]
F --> G[Upload plugin ZIP]
G --> H[PHP in wp-content/plugins runs directly]
First, the login page loads user-profile.js because it also handles password resets. The injected element makes that script run its automatic handling on its own. The URL of the AJAX request it then sends is swapped for an attacker-chosen value by way of the ajaxurl variable that jQuery reads. Overwriting a JavaScript global with a DOM element to hijack it this way is called DOM clobbering.
The request is aimed at WordPress’s REST API, asking for a JSONP response with a parameter like _jsonp=alert. The response comes back as executable JavaScript in the form /**/alert({...}), and jQuery’s globalEval() runs it in WordPress’s own origin. Even where the REST API returns 401, adding _envelope=1 wraps the denial in an outer HTTP 200, so it keeps being processed as script.
The next step uses Same Origin Method Execution (SOME), a technique Paulos Yibelo published in 2022. Because the JSONP callback can contain a property chain, it calls admin operations across windows, as in window.opener.approve.click. When a logged-in admin walks through this sequence, an application password (a REST API credential) is issued and handed off to the attacker’s callback.
With the stolen credential, the rest is just ordinary REST API calls. A single-site admin holds the unfiltered_html capability by default, so they can publish a page containing arbitrary JavaScript. That script lifts the upload form’s nonce (a single-use CSRF token issued per action) and posts the attacker’s ZIP to /wp-admin/update.php?action=upload-plugin. The extracted PHP files sit under /wp-content/plugins/ and are web-accessible directly, so the server runs them.
Separate the unauth XSS from the RCE past it
The same CVE holds both a “reachable without auth” part and a “needs admin action” part. The two carry very different weight.
| Stage | Precondition | What it enables |
|---|---|---|
| Reflected XSS | No account. Just get the error message to render | Run script on the login screen |
| Chain to RCE | Social engineering: lure a logged-in admin to the attacker’s link and get a click | Steal application password, upload plugin, PHP execution |
WordPress’s own advisory frames the escalation to RCE as depending on “conditions outside the attacker’s control” — meaning an admin has to actively click the link. This differs from the conditions for wp2shell, the unauth RCE that landed separately in July (CVE-2026-63030 and others, chained with SQL injection).
What the fix does, and which versions are affected
7.0.3 adds esc_html(), esc_url(), and esc_attr() calls to wp-includes/user.php and wp-login.php. The fix escapes the string left over by the parser disagreement right before it is written out to the page.
Affected versions and the response are in the table below.
| Item | Detail |
|---|---|
| Identifiers | CVE-2026-64638 / GHSA-52p2-r8wf-jcrf |
| CVSS | 8.9 |
| Affected versions | All WordPress versions (fixed builds are provided for branches down to 4.7) |
| Fixed version | 7.0.3 (August 6, 2026), backported through 4.7 |
| Also released | 7.1 RC2 |
| In-the-wild exploitation | None as of August 7 |
| Public PoC | pwn.ai published technical details |
Sites with auto-updates enabled roll up to 7.0.3 or the backport for their branch over time. Manually run sites can apply it from Dashboard → Updates → Update Now. Branches older than 4.7 are out of scope for the fix, so if one is still live it needs a core update.
AI agents ran the whole thing, from search to reproducing the chain
How it was found is the other notable part. pwn.ai says it explored this vulnerability autonomously with open-source models and a multi-agent workflow. Rather than a single-shot bug in the whitespace-tag handling, the agents assembled and reproduced a multi-stage chain — DOM clobbering, SOME, application-password theft, plugin upload.
On the timeline, the chain was reproduced July 26, reported with a PoC July 27, acknowledged as a risk by WordPress the same day, patched August 6, and coordinated-disclosed August 7. Per pwn.ai, the whole effort — from being handed the starting research to completing the chain — took about four days.