XSS2Shell: WordPress Shipped a Sanitiser That Did Not Sanitise
WordPress shipped 7.0.3 on 6 August 2026 to fix CVE-2026-64638, rated CVSS 8.9. It is a cross-site scripting flaw on the login page — the one page every WordPress site exposes to the internet by design — and it chains to PHP execution on the server. The fix was backported through branch 4.7, alongside eleven other security patches.
There is no reported in-the-wild exploitation yet. That is the window you are in right now, and it is the good version of this news.
The bug is in the sanitiser
The trigger is unglamorous: a POST to /wp-login.php with a < character in the log parameter — the username field. Submit a username that does not exist, and WordPress builds an error message that echoes what you typed.
It runs that through strip_tags() first, which is the part worth dwelling on. strip_tags() is not a general-purpose escaper and never claimed to be: it removes things that look like tags. A bare < followed by a tab, a newline or a carriage return does not look like a tag to it, so it survives — and lands in the HTML that gets sent back.
The fix, per the researchers, is esc_html() at the interpolation site. Which is the correct function, and always was.
This is the failure mode worth internalising, because it is not specific to PHP or to WordPress: a function that removes dangerous things is not the same as a function that makes things safe. Stripping operates on a list of what is known to be bad. Escaping operates on the context the value is going into. The first is a filter and inherits every gap in whoever wrote the list; the second is a transformation and does not care what the input contains.
We wrote about the same distinction from a different angle in the MIME-type upload filter bypass — a check that compared the whole Content-Type string instead of its essence. Different language, different decade of code, identical shape: the guard was written against examples rather than against the grammar.
What the chain actually needs
Headlines are describing this as unauthenticated remote code execution on any WordPress site. The researchers' own chain is more specific, and the difference changes what you should do about it.
- JavaScript executes in the WordPress origin. This part is genuinely unauthenticated — anybody can send the request.
- Against a logged-in single-site administrator, the payload steals an Application Password.
- That credential is used to upload a plugin.
- The plugin gives PHP execution on the server.
So the XSS needs no account, but the escalation needs an administrator's browser to run the payload. That is a real precondition and it is also an easy one to meet: administrators visit their own login page constantly, and a link is a link. It is the difference between “every site falls the moment someone scans it” and “every site falls the first time an admin follows a link”. Both are bad. Only one of them is instant.
The middle step is the clever one. Same Origin Method Execution hijacks a user-profile script that WordPress already enqueues on the login page — code that normally handles password resets, repurposed because it is sitting in the same origin as the injected payload. And Application Passwords are the ideal thing to steal: they are API credentials that survive the session, so a momentary XSS becomes durable authenticated access.
Which versions
Per the researchers, 6.4 through 7.0.2 are exploitable via this path — the chain depends on wp_kses_post() behaviour introduced in 6.4, so older branches are not reachable the same way.
Some coverage says the flaw has shipped since 4.7. That figure comes from how far the backport went, not from how far the exploit reaches. WordPress patched down to 4.7 because that is every branch still receiving security updates, which is the responsible thing to do and not evidence of a wider blast radius. Worth knowing if you are assessing an old install: still patch it, but the urgency is highest on 6.4+.
What to do
- Update to 7.0.3, or take the backported release for your branch. If auto-updates for core are on — and after wp2shell in July many sites had them forced on — confirm the version rather than assuming the update landed.
- Audit Application Passwords now. This is the step people will skip. Every administrator account: Users → Profile → Application Passwords. Revoke any you do not recognise, and any you no longer use. They do not expire on their own and they are not visible in a normal session list.
- Check for plugins you did not install. The final step of the chain is a plugin upload. Compare the plugin list against what you expect, and look at file modification times in
wp-content/pluginsrather than trusting the admin UI, which a malicious plugin can influence. - Put the login page behind something. IP allow-listing, HTTP auth, or a WAF rule in front of
wp-login.php. The researchers note that alogparameter containing<at all is a reliable signal, since valid usernames never contain a bracket — that is a cheap detection rule and a cheap block. - Do not wait for exploitation reports. There is a public technical write-up. The gap between that and a working exploit is measured in days, and the last WordPress core RCE went from disclosure to mass scanning inside a week.
The uncomfortable arithmetic
WordPress runs something over 43% of the web. A pre-auth XSS in the one file that every one of those installations exposes is about as broad a surface as exists.
What makes it survivable is that the disclosure came with a patch, the patch was backported to every supported branch, and nobody has seen it used yet. That is the system working. The part that will decide the outcome is not the WordPress security team's work, which is already done — it is how many of those installations have someone who reads release notes.
