Comment2shell Zero Click Pre Auth Xss To Rce In Wordpress Core
WordPress 7.1.1 fixes CVE-2026-93485, an unauthenticated stored XSS in wpautop(). The fix was backported to every supported branch from 4.7 onward, so a site below its own branch's patched version is still affected.
An unauthenticated visitor can craft a that is transformed from benign HTML into malicious HTML with a live JavaScript event handler when a block theme or certain classic themes render it.
Execution needs no interaction beyond loading the page. Code execution on the server can be achieved through the known WordPress escalation by uploading a plugin file through the administrator's session.
This blog post is an unauthenticated stored XSS vulnerability in WordPress core, tracked as CVE-2026-93485. If you use WordPress, please update to at least version 7.1.1, or to the latest release in your branch. The fix was backported to every supported branch down to 4.7.36.
Editorial (IDNSEC): This issue was reported by the author, Rafie Muhammad, a security researcher at Awesome Motive Inc., through the WordPress bug bounty program on HackerOne and fixed under coordinated disclosure.
WordPress sanitizes a when it is saved, using its HTML sanitizer (KSES), then formats it when it is displayed, using the comment_text filter chain. The vulnerability occurs in the gap between those two steps.
A user can send raw HTML as a , but the allowlist in wp-includes/kses.php:605-633 only allows a[href,title] , abbr[title] , acronym[title] , blockquote[cite] , del[datetime] , q[cite] , and code .
When the is rendered, the formatting chain is registered on the comment_text filter:
wp-includes/default-filters.php:225-230
All of these filters rewrite HTML. Those rewrites contain a flaw that transforms benign HTML into malicious HTML that enables JavaScript execution.
The key to this exploit is the newline ( \n ) in the cite attribute of , which causes an insecure cascading transformation in the filters. With the correctly aligned payload, valid markup containing a JavaScript handler can be formed and processed in the final render.
Below is the high-level insecure transformation that can happen, where the event handler string that is initially processed as normal text inside a tag can be moved to become an attribute in the .
(1) KSES allows a blockquote's cite attribute and newline
A containing TEXT is allowed by KSES. The blockquote[cite] is one of the allowed HTML components.
wp_kses_hair() reads each attribute with get_attribute() , which HTML-decodes character references, then re-encodes the result with a strtr() over certain syntax characters:
wp-includes/kses.php:1721-1727
The \n is not in that map, so a newline inside an attribute value like cite="a\nb" can be used.
Alternatively, the encoded form of a newline, such as cite="a b" , can also be used because WordPress will store it with the actual \n .
wpautop() takes every newline that sits inside a tag and swaps it for an HTML :
wp-includes/formatting.php:502
What the rest of the function sees in its place is , a that carries a > of its own, which plays an important part in the step.
(3) wpautop() injects an unexpected tag
wpautop() puts a blank line above every block-level opening tag, and blockquote is on that list:
wp-includes/formatting.php:490
So the always begins a paragraph of its own, whatever the contains, and the rebuild loop wraps that paragraph in a :
wp-includes/formatting.php:539-548
The whole stays in one piece and comes back out as b"> .
There is an improper preg_replace in the formatting code.
wp-includes/formatting.php:563
[^>]* cannot cross a > , so it stops at the first one it meets, which is the > in the . The capture comes back as cite="a is written straight after it, in the middle of the cite value. The placeholders are turned back into newlines at the end of the function, which leaves this:
The injected here is the key to the step.
(4) wptexturize() encodes the double quote
When the site uses a block theme or a classic theme with a full Latest block, there is a call to wptexturize() .
A block theme calls it via get_the_block_template_html() by default.
wp-includes/block-template.php:297-299
Certain classic themes also call wptexturize() if the_content() is called, such as the Twenty Twenty-One theme.
wptexturize() sees the injected as a tag boundary and treats " as ordinary text. It changes that quote to a typographic entity, ” . The quote inside remains straight, as wptexturize() deliberately excludes the contents of certain elements:
wp-includes/formatting.php:106
Since code is on the allowlist, a element can deliver a raw " that survives every filter. It is not the only option. Any attribute delimiter in the works too, because a quote inside a tag is not in a text position, so wptexturize() never treats it as a quote to be encoded. KSES also allows an attribute value to contain spaces, = , ( , and ) , so the text that ends up in an attribute name position can form a working event handler.
(5) Final malicious HTML rendering
After all the chained filters above, the final HTML now contains a JavaScript event handler. See the proof of concept below.
The is posted anonymously, with no cookie and no nonce:
The is stored exactly as it was submitted. Fetching the post without any cookie gives us the cite value with a paragraph tag inside it:
In a modern browser, the now carries onfocus , autofocus , and tabindex as real attributes. autofocus puts focus on the element while the page is still loading, so onfocus fires without any interaction, and the alert shows the site's own origin from document.domain .
Both the newline and the quote are needed. If the newline is removed, the tag stays intact and the payload stays inside the text. If the closing quote is moved out of the element, wptexturize() curls it as well and no attribute is formed.
Scenarios where approval is not required
moderation is off at stock settings because comment_moderation defaults to 0 , so WordPress does not hold every . What gates a first-time commenter is the separate comment_previously_approved , which defaults to 1 ( wp-admin/includes/schema.php:441 and :546 ). This is why WordPress titled the advisory for the WordPress 7.1.1 security update "subject to approval." The CVE record by Patchstack later adds that this requirement "can be bypassed."
There are at least three routes where the payload needs no moderator at all.
Reuse the commenter the installer created. A stock install ships with an approved from A WordPress Commenter at [email protected] . A submitted under that name and address is approved immediately, because check_comment() matches on the author name and email together.
The setting is turned off. With " author must have a previously approved " unchecked in Settings, Discussion , a brand-new identity is stored as approved.
The stays held. A pending still renders for anyone who carries a comment_author_ cookie, which is anyone who has ever left a on the site, through the attacker's own ?unapproved= &moderation-hash= link.
The handler runs in the session of whoever loads the page. If that visitor is a logged-in administrator, the script can reach the plugin installer. Uploading a plugin is the known escalation from an administrator-context XSS to code execution on the server.
The upload endpoint checks upload_plugins ( wp-admin/update.php:151 ), which map_meta_cap() resolves to the install_plugins capability that an administrator holds. So the handler can read the upload nonce out of the installer form and POST a zip file that contains a PHP shell:
The plugin does not even need to be activated. The file is already on disk, and plugin files are reachable directly, so the shell answers right away:
Whatever is passed in c runs as the web server user.
WordPress 7.1.1 fixed the issue with a one-line change. The rewrite now uses a quote-aware subpattern, so it can no longer match a > that sits inside a quoted attribute value. The patch can be seen below:
wp-includes/formatting.php:563
This matches the fix suggested in the disclosure report, with the delimiter moved from | to ! because the new subpattern contains a | of its own.
One line is enough because that rewrite is the only place in wpautop() that inserts something inside a tag. With the new subpattern, the capture runs to the end of the quoted value, cite="a b" , and the lands after the > that really ends the tag.
8 September 2026 — Reported to the WordPress core team through the WordPress bug bounty program on HackerOne. The security team triaged it on the same day.
15 September 2026 — A CVE was requested from Patchstack.
17 September 2026 — WordPress 7.1.1 was released with the fix, together with backports for every supported branch down to 4.7.36.
18 September 2026 — Patchstack assigned CVE-2026-93485 .
21 September 2026 — This research article was published.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
