HIGH: WordPress 7.0.3 Fixes Pre-Auth XSS Chaining to PHP Code Execution
WordPress 7.0.3 patches CVE-2026-64638, a pre-authentication cross-site scripting flaw on the login screen that researchers chained all the way to PHP code execution on the server. The CVSS 8.9 bug comes from a parser disagreement between strip_tags and KSES, affects every maintained version before 7.0.3, and was backported to the 4.7 branch. No confirmed exploitation in the wild yet, but the full technique is public.
WordPress pushed a security release on August 6, and if you maintain sites for a living, this one deserves more than the usual glance at the changelog. Version 7.0.3 fixes twelve issues in core. Eleven of them are the ordinary housekeeping you expect from software that runs a large slice of the public web. The twelfth is a pre-authentication cross-site scripting flaw on the login screen that researchers chained all the way to PHP code execution on the server, and it has been sitting in the codebase for the better part of a decade.
The bug is tracked as CVE-2026-64638 and carries a CVSS score of 8.9, which puts it firmly in the "this is bad, prioritize it" tier. It was found by the team at pwn.ai, who named the chain XSS2Shell and published a walkthrough that is genuinely worth reading if you enjoy watching an exploit get assembled out of parts that are each individually harmless.
A disagreement between two sanitizers
The root cause is a parser differential, which is a polite way of saying two pieces of code looked at the same string and reached different conclusions about what it was.
When someone submits a username that does not exist, WordPress builds an error message for the login screen. That username first passes through wp_strip_all_tags, which leans on PHP's native strip_tags function. PHP's parser only treats a less-than sign as the start of a tag when a letter follows it immediately. Put a space in between, write "< area" instead of the normal form, and strip_tags shrugs and leaves the string alone as harmless text.
The message then passes through KSES, WordPress's own HTML filter, on its way to the page. KSES is more forgiving about whitespace, so it reads that same string as a perfectly valid area element. Area tags sit on the allowlist because image maps exist, and the allowlist permits attributes like id, class, href, and name. So the sanitizer that was supposed to strip markup let it through as text, and the sanitizer that was supposed to allowlist safe markup turned that text back into live DOM. An unauthenticated attacker gets to place elements of their choosing into the login page with a single failed login attempt.
Injecting an area tag is not the same as injecting a script tag, and this is where the research stops being a curiosity and starts being a problem.
From allowlisted markup to script execution
WordPress enqueues user-profile.js on the login page to support the password reset flow. That script carries a delegated click handler bound to an element with the id color-picker, watching for clicks on elements with the class color-option, plus a ready handler that looks for reset-pass-submit and automatically fires a click on a wp-generate-pw button. None of that is a vulnerability. It is the password generator doing its job.
The injected elements are crafted to match those exact selectors. The ready handler fires, the click bubbles into the delegated handler, and the handler calls jQuery's post method against a variable named ajaxurl. On the login screen that variable is never defined, so the browser resolves it through DOM clobbering to the injected area element, which conveniently carries the id ajaxurl. jQuery calls toString on it, the browser resolves that through the element's href property, and the request goes wherever the attacker pointed it.
Where it points is WordPress's own REST API, using the rest_route parameter to hit the public index that requires no authentication, the _method parameter to convert the request to a GET, and the _jsonp parameter to name a callback. The response comes back typed as JavaScript, jQuery evaluates it, and the attacker now has script execution in the site's origin without ever holding a session.
The rest of the chain is a patient sequence of legitimate WordPress features doing exactly what they were designed to do. The JSONP callback name passes a filter that permits dots, which means it can be a property path rather than a bare function name, and that opens the door to same origin method execution against a window the attacker opened. The target is the approve button on authorize-application.php. Clicking it as a logged-in administrator mints an Application Password and hands it to a callback URL of the attacker's choosing. From there the attacker authenticates to the REST API over HTTP basic auth, and because single-site administrators hold the unfiltered_html capability by default, publishes a page containing script. Navigating the administrator to that page runs the script with the administrator's cookies, which is enough to pull the plugin upload form, lift its nonce, and post a ZIP archive. WordPress checks the nonce and the capability, both of which are entirely valid, and unpacks the archive into wp-content/plugins. PHP files dropped there are reachable over the web whether or not anybody activates the plugin. That is the shell.
Who is actually at risk
A reality check is in order, because the two halves of this chain have very different requirements. The XSS half needs nothing at all. Any attacker who can reach your login page can inject DOM into it, and that alone is enough for credential harvesting, convincing fake login overlays, and general drive-by mischief against anyone who happens to be looking at wp-login.php at the time. The full code execution chain is more demanding. It needs an administrator who is already logged in and who clicks through to a page the attacker controls. That is a phishing problem, and phishing problems have a long history of getting solved by attackers.
Every maintained WordPress version before 7.0.3 is affected, and the fix was backported all the way to the 4.7 branch, which tells you how long this has been quietly sitting in core. The researchers reported it on July 27 after reproducing the full chain the day before, WordPress acknowledged the risk the same day, and the fix landed on August 6 with coordinated disclosure the following day. As of publication there is no evidence of exploitation in the wild and no public proof of concept beyond the researchers' own writeup. That gap tends to close in days rather than months once a technique has been documented in this much detail.
The other eleven fixes are not nothing either. There are four stored XSS issues that need contributor-level access, reached through the emoji settings element, the Post Content block, Quick Edit on sites with large user counts, and the Post Date block. There is a privilege escalation that only bites multisite installs with open user registration, three information disclosure bugs covering protected content in the Latest Comments block, post slug enumeration, and private comment feeds, a CSS injection that lets an author bypass the safe CSS filters, an email verification bypass, and a server-side request forgery in URL validation that reaches link-local address ranges. On a multi-author site, several of those are exactly the kind of thing a low-privileged account turns into a much bigger problem.
What to do about it
Update to 7.0.3, or to the patched release on whichever maintenance branch you are running. Sites with background updates enabled should have taken it automatically, which is the good news. The bad news is that plenty of managed hosting environments and agency builds disable background updates for stability reasons, and those sites are still sitting exposed. Go verify rather than assume, because "auto-updates are on" is a belief that fails very quietly.
While you are in there, consider setting DISALLOW_FILE_MODS in wp-config.php on sites that do not need dashboard-driven plugin installs, since that closes the final step of the chain. Be aware it blocks legitimate updates through the dashboard too, so it belongs on sites where deployment happens through a pipeline rather than through the admin panel. Restricting or disabling Application Passwords is worth considering as well, because that feature is the hinge the whole escalation swings on and most small business sites have no use for it.
For detection, the login-side attack leaves fingerprints. Look for login POSTs where the username field contains a less-than sign followed by whitespace, since malformed tags are the whole trick and normal users do not type them. Watch for REST requests carrying _jsonp, _method, or _envelope parameters, none of which show up in ordinary browsing. Alert on Application Password creation events, which are rare enough in most environments that reviewing every one of them is reasonable. And audit the plugin directory against what you actually installed, because a folder nobody remembers deploying is the loudest signal you are going to get.
If any of that turns up, treat it as a full compromise rather than a scare. Rotate the database credentials, invalidate every session by rotating the authentication salts in wp-config.php, review the administrator list for accounts and Application Passwords nobody created, and go through the plugin directory looking for stray PHP files. Restoring from a backup taken before you can prove the site was clean is not a fix, it is a delay.
None of this is exotic. It is a decade-old inconsistency between two sanitizers, discovered because somebody was patient enough to ask what happens when you put a space in an unusual place. That is generally how these things go.
For the MSP crowd, this is the easiest managed WordPress conversation you will have all quarter, because the client who insisted their marketing site was "just brochureware" now has a documented path from an anonymous failed login to a web shell on the same box. Package the patching, the plugin inventory, the Application Password audit, and the login-page monitoring into a flat monthly WordPress maintenance plan rather than billing it as incident cleanup later, since twelve core fixes in a single release is a very concrete argument that nobody is keeping up with this by hand.
References
- NVD CVE-2026-64638
https://nvd.nist.gov/vuln/detail/CVE-2026-64638
- WordPress 7.0.3 Version Documentation
https://wordpress.org/documentation/wordpress-version/version-7-0-3/
- pwn.ai Research, XSS2Shell WordPress Preauth XSS to RCE Chain
https://pwn.ai/blog/xss2shell
- The Hacker News, New WordPress Pre-Auth XSS Could Lead to PHP Code Execution
https://thehackernews.com/2026/08/new-wordpress-pre-auth-xss-could-lead.html
- Patchstack, WordPress 7.0.3 Released with 12 Vulnerabilities Fixed
https://patchstack.com/articles/wordpress-7-0-3-released-12-vulnerabilities-found-and-fixed/
Concerned about this threat?
Our security team can assess your exposure and recommend immediate actions.
Protect Your Organization
Find vulnerabilities like this in your systems before attackers do.
24/7 monitoring to detect and respond to threats like these in real time.
Block phishing and malware delivery targeting your organization.
Map security controls to 26 frameworks including NIST, SOC 2, and HIPAA.