Almost every filter that protects a website works on the same assumption: that you can look at a piece of input and decide whether it is dangerous. Strip the <script> tags, reject the onclick, refuse anything that looks like an event handler, and what is left is safe to store.

The bug WordPress patched on September 17 is interesting because the input really is safe to store. There is nothing in it to strip. It is a comment containing a quotation and a line break. It becomes dangerous later, when WordPress reformats it for display, and the thing that makes it dangerous is WordPress’s own formatting code doing exactly what it was written to do.

That is a different class of problem from the ones we usually write up here, and it is worth understanding properly, because the instinct it breaks is the one most people rely on. “We moderate our comments” does not help you when the comment you approved was genuinely fine at the time you read it.

Update, September 24, 2026. WordPress 7.1.1 is no longer the version you want. Five days after this release, on September 22, WordPress shipped 7.1.2 as a single-fix emergency release for CVE-2026-87902, an unauthenticated path traversal in page-template resolution that becomes remote code execution on a site whose theme has a top-level directory beginning with page-. We pulled the release archive’s own last-modified header: wordpress-7.1.2.zip was published at 13:25:21 UTC on September 22. The fix was backported to 25 releases going back to 4.7.37, so every affected branch has a patch. It is being exploited: Patchstack logged the first attempt at 11:49 UTC on the day of release and the first successful file write at 15:34 UTC, and a named Nuclei template for it is in circulation. Everything in this article about 7.1.1 still stands, and 7.1.1 is now itself out of date. The full write-up on the newer flaw is here, including which themes meet the precondition and how to check yours in one command. Updated September 26: CISA added CVE-2026-87902 to the Known Exploited Vulnerabilities catalogue on September 25 with a federal deadline of September 28, so the newer flaw is now formally a known-exploited one. Comment2Shell itself, CVE-2026-93485, is still not listed. Re-checked September 28 at catalog version 2026.09.27, 1,728 entries: CVE-2026-93485 is still not listed, ten days after the release that fixed it. It remains a flaw with a patch, a CVE and no confirmed exploitation, which is the right reason not to be in that catalogue and the wrong reason to leave it unpatched.

Everything below was checked on September 22, 2026. The release facts come from WordPress.org’s own release documentation, pulled directly. The mechanism is quoted out of the WordPress source through the official developer reference, also pulled directly, and we say which parts are our own reading of that code rather than something a researcher published.

What shipped

WordPress 7.1.1 was released on September 17, 2026. WordPress.org describes it as a security and maintenance release containing 17 bug fixes on Core and 11 security fixes, and its advice is unambiguous:

Because this is a security release, it is recommended that you update your sites immediately.

The scale of the backport is the part that tells you how old the bug is. Fixes were published for every supported branch back to 4.7.36. That is twenty-four separate releases on the same day: 7.0.5, 6.9.8, 6.8.9, 6.7.8, 6.6.8, 6.5.11, 6.4.11, 6.3.11, 6.2.12, 6.1.13, 6.0.15, 5.9.17, 5.8.16, 5.7.18, 5.6.20, 5.5.21, 5.4.22, 5.3.24, 5.2.27, 5.1.25, 5.0.28, 4.9.32, 4.8.31 and 4.7.36. WordPress.org notes that 4.6 and earlier no longer receive security updates at all.

The headline issue is listed in the credits like this, verbatim:

An unauthenticated stored cross-site scripting (XSS) issue via paragraph formatting, subject to comment approval, reported by Rafie Muhammad (Awesome Motive, Inc.)

Patchstack assigned it CVE-2026-93485 and scored it 7.1 on CVSS 3.1, with the vector AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L. The researcher who found it nicknamed it Comment2Shell.

For scale, W3Techs reported on the day we checked that WordPress runs 40.2% of all websites and 58.8% of sites where the content management system is known. This is core, not a plugin. If your business site runs WordPress and somebody has not updated it since Wednesday, it is affected.

The bug, precisely

wpautop() lives in wp-includes/formatting.php. Its job is mundane and enormous: it turns blank lines into paragraphs and single line breaks into <br> tags, and it runs on the content of nearly everything WordPress renders, including comments.

Two pieces of that function matter here, and both are visible in the published source for WordPress 7.1.

First, the newline protection. Line breaks inside an HTML tag would confuse the paragraph logic, so before it starts, wpautop() replaces them with a placeholder:

wp_replace_in_html_tags( $text, array( "\n" => ' <!-- wpnl --> ' ) )

Every newline that sits inside a tag becomes the literal string <!-- wpnl -->. At the end of the function they are swapped back. This is sensible, it has been there for years, and the important detail is that the placeholder contains a greater-than character.

Second, the blockquote handling. Later in the same function, this pattern moves an opening blockquote so it nests correctly with the paragraph around it:

"|<p><blockquote([^>]*)>|i"

Read [^>]* carefully. It means “any run of characters that are not a greater-than sign,” and it is standing in for “the attributes of this tag.” That is the assumption. In ordinary HTML written by ordinary people it holds, because a greater-than sign does not usually appear inside a tag’s attributes.

Now put the two together. An attacker writes a comment containing a <blockquote> with a line break inside one of its attribute values. The first step rewrites that line break into <!-- wpnl -->, which injects a greater-than character inside the quoted attribute value. The second step then scans for the end of the tag, hits that greater-than character, and concludes the tag ended early. The tag gets split at the wrong place, and the text that the author intended to sit safely inside a quoted attribute value now sits outside it, in the position where a browser expects to find attribute names. Text in that position that looks like an event handler is an event handler.

That is the whole bug. No script tag was ever submitted. No filter was evaded. The dangerous markup did not exist when the comment was saved; it was assembled at display time by two correct-looking pieces of code that each made a reasonable assumption the other one broke.

Why the sanitiser was never going to catch this

WordPress does filter comments. wp_kses() strips comment HTML down to a short allow list: mostly inline elements, plus, as WordPress’s own documentation puts it, all block-level elements stripped except blockquote.

So blockquote is the one block-level tag a stranger is permitted to use in a comment. It is also the tag in the vulnerable regex. That is not a coincidence in any interesting sense, but it does explain why the bug is reachable by an anonymous visitor rather than only by an author writing a post.

The deeper reason the sanitiser could not help is ordering. wp_kses() runs when the comment is saved and inspects what was submitted. wpautop() runs when the comment is displayed and rewrites what was saved. A sanitiser that validates input can only defend against input that is already dangerous. It has no way to know that a later stage will take its approved output and rearrange it into something else.

We have now covered three separate WordPress-ecosystem bugs in six weeks that share that shape. A plugin that stored an exploit in the comments table where it sat inert until a backup restore ran it. A plugin that wrote an administrator password reset link into a table any logged-in user could read. And core’s own pre-auth RCE in August. In every one, the data was harmless where it was written and became harmful somewhere else. Input validation is necessary and it is not sufficient, and a stack this old has a lot of somewhere-elses.

“Subject to comment approval” and why that is thinner than it reads

The official credit line includes the qualifier subject to comment approval, and it would be easy to read that as “so only sites with careless moderation are at risk.” Patchstack addressed this directly. Their words:

moderation isn’t a security control. Approving comments is routine work

They give two reasons the requirement does not hold up. The first is structural: on a default WordPress install, a commenter whose first comment has been approved is auto-approved from then on. The gate is one-time, per person, not per comment. Anyone willing to leave one boring comment and wait for it to be approved has bought themselves unlimited future submissions.

The second is human. Patchstack notes the payload “looks unremarkable in the moderation queue.” There is no script tag to spot. It is a quotation with a line break in it. A person clicking Approve on a Tuesday morning has no signal to act on.

There is a third point, and this one is ours rather than theirs, taken from the public code rather than from any advisory. The admin comments screen renders each comment by calling comment_text(), and comment_text() applies the comment_text filter. WordPress attaches wpautop to that filter at priority 30 by default, alongside make_clickable at 9 and convert_smilies at 20. Which means the formatting step that assembles the payload is not something that only happens on the public side of the site. It happens in the moderation queue too, in a browser that is already logged in as an administrator.

We are stating that as a reading of published code, not as a demonstration. Patchstack deliberately withheld the exploitation chain, writing: “We aren’t publishing the breakout chain or a working payload.” We are not publishing one either. But the code path above is in the developer reference that ships with every copy of WordPress, and it is the reason “we always moderate” is a weaker answer here than it sounds.

What the score says, and what the nickname says

This is worth separating carefully, because the two available descriptions of this bug point in different directions and both are being repeated.

The CVSS vector Patchstack assigned, AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L, describes a network-reachable bug needing no privileges, requiring some user interaction, that crosses a security scope boundary and causes low impact to confidentiality, integrity and availability. That is a normal, accurate description of stored cross-site scripting. It is not a description of remote code execution. Nothing in that vector says “web shell.”

The nickname, Comment2Shell, and the coverage built on it, describe the realistic end state: script running inside an authenticated administrator session can do whatever that session can do, and inside wp-admin that includes installing a plugin, and a plugin is arbitrary PHP.

Both of these are true and they are answering different questions. The score rates the vulnerability. The nickname rates the consequence. The honest position, and the one we are taking, is that no public proof of the full chain exists today, the escalation step is entirely plausible and well understood as a general technique, and you should not need to resolve that argument before you update, because updating is free and takes a minute.

On exploitation: as of today, there is no public report of this being exploited in the wild, and it is not in CISA’s Known Exploited Vulnerabilities catalog. (Re-checked September 26 at catalog version 2026.09.25: CVE-2026-93485 is still not listed. The path traversal that followed it five days later now is.) That is the good news and it has a short shelf life. The patch is public, which means the diff is public, and a one-line regex change in formatting.php is about as legible a diff as a vulnerability ever gets.

The other ten, and who found them

Comment2Shell is the only one of the eleven that an anonymous visitor can reach. The rest require an account of some kind, which on a normal small business site with three staff logins is a much narrower problem. They are still worth glancing at, because two names in the credit list are the actual story of 2026.

WordPress credited Anthropic for two of the eleven: a Contributor-level arbitrary post overwrite, and an authenticated path traversal in the REST API templates controller. It credited Paulos Yibelo and pwn.ai for a flaw where a crafted URL could install and preview a theme from WordPress.org. Anthropic was also credited in WordPress 7.0.3 on August 6, 2026, for an Author-level bypass of the safe CSS attribute filter, in a release that also credited Aikido Security.

AI labs are now regular contributors to the credit list of the most widely deployed web application on earth. We wrote in September about what the exploitation data actually shows about AI-discovered vulnerabilities, and the conclusion there holds up well here: the interesting effect so far is not novel attacks, it is volume and speed on the defensive side. Eleven fixes in one release, two of them from a model-assisted reviewer, is what that looks like in practice.

The remaining credits went to Jeremy Felt and Ben Bidner of the WordPress Security Team, Jesse McNeil, HDWSec, hermanhms and viridis.

What to actually do

1. Check what version you are on. The fastest honest check, from any machine:

curl -s https://example.com/ | grep -o '<meta name="generator" content="WordPress [^"]*"'

That reads the generator tag, which many sites remove, so a blank result means “unknown,” not “safe.” If you have shell access, WP-CLI is definitive:

wp core version
wp core check-update

2. Update. If you are on the current line you want 7.1.1. If you are pinned to an older branch for plugin-compatibility reasons, take the patched release on your own branch from the list above. Via WP-CLI:

wp core update
wp core update --minor   # stays on your branch

Or Dashboard, then Updates, then Update Now.

3. Confirm the automatic update actually ran. This is the step people skip. WordPress has shipped background updates for minor and security releases since 3.7 and they are on by default on most installations, which is why many sites were patched on the 17th without anyone touching them. They are also switched off, silently, by several common conditions: AUTOMATIC_UPDATER_DISABLED set in wp-config.php, WP_AUTO_UPDATE_CORE set to false, a version control checkout detected in the WordPress directory, or a filesystem the web server cannot write to. None of those announce themselves. Go and look at the version number rather than assuming.

4. If you accept public comments, treat this as the priority item in the release. Patchstack’s framing, quoted: “Sites that accept public comments should treat this as the priority fix.” If you do not accept comments at all, the other ten fixes still apply to you, but the anonymous route does not.

5. If the site was already on an old unpatched version for a long time, updating does not answer the question of whether something already happened. That is the same point we made about the August core RCE and it has not got any less true. Look for administrator accounts you do not recognise, plugins in wp-content/plugins that nobody installed, and recently modified PHP files:

wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp plugin list --fields=name,status,version
find wp-content -name '*.php' -mtime -30 -printf '%TY-%Tm-%Td %p\n' | sort

A clean result there is reassuring rather than conclusive, but an unfamiliar administrator account is not ambiguous at all.

The hardening that works, and the trap inside it

Here is the part that is genuinely useful and that we have not seen written up anywhere in the coverage.

If you want to break the dangerous half of this bug rather than just patch this instance of it, the lever is the plugin installer. Script running in an administrator session becomes a web shell by installing a plugin. Take that ability away and the worst case collapses back down to something much closer to the CVSS score. WordPress has a constant for exactly this, and its documentation describes it like so:

This will block users being able to use the plugin and theme installation/update functionality from the WordPress admin area. Setting this constant also disables the Plugin and Theme File editor (i.e. you don’t need to set DISALLOW_FILE_MODS and DISALLOW_FILE_EDIT, as on its own DISALLOW_FILE_MODS will have the same effect).

define( 'DISALLOW_FILE_MODS', true );

That is a strong control on a finished business website, where nobody should be installing plugins from the dashboard at two in the morning anyway.

Now the trap. Follow the code. WP_Automatic_Updater::is_disabled() begins:

public function is_disabled() {
	// Background updates are disabled if you don't want file changes.
	if ( ! wp_is_file_mod_allowed( 'automatic_updater' ) ) {
		return true;
	}

And wp_is_file_mod_allowed() is:

function wp_is_file_mod_allowed( $context ) {
	return apply_filters( 'file_mod_allowed', ! defined( 'DISALLOW_FILE_MODS' ) || ! DISALLOW_FILE_MODS, $context );
}

So defining DISALLOW_FILE_MODS turns off background automatic updates entirely. Including security releases. Including the one that fixed this bug.

Read that as a whole and it is a genuinely awkward result: the single most effective hardening measure against the consequence of Comment2Shell is also the measure that would have stopped your site from receiving the fix for Comment2Shell. Sites that hardened themselves this way in 2023 and have not thought about it since are likely to be sitting on both the constant and an unpatched core.

There are two honest ways out, and you have to pick one deliberately.

The first is to keep DISALLOW_FILE_MODS and move updates to a process you actually run: a scheduled wp core update --minor over SSH, or a managed host that patches core outside the dashboard, or a deployment pipeline. This is the right answer if you have somebody who will genuinely run it.

The second is to keep the constant and hand the updater back its exception. Notice that wp_is_file_mod_allowed() passes a $context string through the file_mod_allowed filter, and the automatic updater passes 'automatic_updater'. So in a small must-use plugin:

<?php
// mu-plugins/allow-core-autoupdates.php
// Keep DISALLOW_FILE_MODS for the dashboard installer and file editor,
// but let background core security updates continue to run.
add_filter( 'file_mod_allowed', function ( $allowed, $context ) {
	return 'automatic_updater' === $context ? true : $allowed;
}, 10, 2 );

That is our own construction, not something WordPress documents as a recommended pattern, and you should treat it accordingly: test it on staging, confirm with wp core check-update that the updater still sees releases, and be aware that you are deliberately re-opening a narrow path for core’s own updater while leaving the dashboard installer closed. We think that trade is correct for a small business site. We are telling you it is a trade rather than pretending it is free.

If neither option appeals, the fallback is the unglamorous one: do not set the constant, and put a recurring calendar entry on the third Wednesday of the month to check the version number. That is roughly the cadence we argued for with Linux kernel patching and the logic is identical. A cadence you keep beats a control you forget you enabled.

What we could not verify

  • The full XSS to RCE chain. Patchstack withheld it on purpose. We did not reconstruct it and we are not publishing a payload. The escalation is plausible and standard; it is not, today, publicly demonstrated for this specific bug.
  • Whether the moderation queue is a confirmed firing point. Our claim in that section is a reading of the comment_text filter registrations and WP_Comments_List_Table::column_comment() in the published source. We did not test it against a live vulnerable install, and no advisory we read states it.
  • How many sites are still unpatched. Secondary sources disagreed by a wide margin on WordPress version distribution, quoting figures from 51% to 68% for the WordPress 7 branch alone, and we could not retrieve the underlying chart from WordPress.org’s statistics page. We are not repeating a number we could not pull.
  • The Block Editor bug count in 7.1.1. WordPress.org’s release documentation says 21 and the release announcement as we retrieved it says 19. Immaterial to the security content, but we noticed and we are not going to quietly pick one.
  • Exactly which additional conditions are required. One secondary write-up claims block themes from Twenty Twenty-Two onward are at particular risk because of how they call wptexturize() during rendering. We could not corroborate that from Patchstack, from WordPress.org, or from the source, so we have left it out of the body rather than repeat it.
  • Whether CVE-2026-93485 is stored or DOM-based XSS. At least one vulnerability database classifies it as DOM-based. WordPress and Patchstack both describe it as stored, and the mechanism we traced is unambiguously server-side rendering of stored content. We went with the primary sources.

The short version

A stranger can leave a comment on a WordPress site that contains nothing dangerous, passes the comment sanitiser honestly, sits in the database as ordinary text, and turns into a live script the moment WordPress formats it for display. The formatting function has been doing this since at least version 4.7, which is to say since 2016.

Fixed on September 17 in 7.1.1, and in patched releases on twenty-three older branches. Not currently known to be exploited. The patch is public and the diff is small, so that is a statement about today.

Three things to do, in order. Check the version number rather than assuming the automatic update ran. Update to 7.1.1 or to the patched release on your branch. And if your wp-config.php contains DISALLOW_FILE_MODS, understand that you have been running without background security updates for as long as that line has been there, and decide which of the two ways out you want.

If you are not sure which of those applies to your site, get in touch. We are in Toronto, we run this check across every WordPress site we look after, and the version number takes about thirty seconds to establish.

Sources

  • WordPress 7.1.1 Maintenance and Security Release, WordPress.org, September 17, 2026, and Version 7.1.1 in the WordPress documentation, both retrieved September 22, 2026, for the release date, the 17 Core bug fixes and 11 security fixes, the verbatim “Because this is a security release, it is recommended that you update your sites immediately,” the full credit list including the exact wording of the Comment2Shell entry and the two entries reported by Anthropic, the complete backport list down to 4.7.36, the note that 4.6 and earlier no longer receive updates, and the list of revised files including wp-includes/formatting.php
  • WordPress 7.1.1 Maintenance and Security Release, Patchstack, retrieved September 22, 2026, for the CVE-2026-93485 identifier and the CVSS 3.1 score of 7.1 with vector AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L, the identification of wpautop() in wp-includes/formatting.php and the |<p><blockquote([^>]*)>|i pattern, the quoted “moderation isn’t a security control. Approving comments is routine work,” the auto-approval behaviour for returning commenters, the observation that the payload looks unremarkable in the queue, the quoted “We aren’t publishing the breakout chain or a working payload,” and the quoted “Sites that accept public comments should treat this as the priority fix”
  • wpautop() in the WordPress developer reference, source as published for WordPress 7.1, retrieved September 22, 2026, for the wp_replace_in_html_tags( $text, array( "\n" => ' <!-- wpnl --> ' ) ) newline placeholder, its restoration via str_replace, and the blockquote regex quoted above
  • wp_kses_allowed_html(), for the description of the comment context as allowing most inline elements and stripping block-level elements except blockquote
  • comment_text and WP_Comments_List_Table::column_comment(), for the admin rendering path, together with the default registrations of make_clickable at priority 9, convert_smilies at 20 and wpautop at 30 on the comment_text filter
  • wp_is_file_mod_allowed() and WP_Automatic_Updater::is_disabled(), source retrieved September 22, 2026, both quoted verbatim above, for the finding that DISALLOW_FILE_MODS disables background automatic updates
  • wp-config.php API reference, for the quoted description of DISALLOW_FILE_MODS, and Upgrading WordPress, for automatic background updates being enabled by default since 3.7 and for AUTOMATIC_UPDATER_DISABLED, WP_AUTO_UPDATE_CORE and version control detection as the conditions that switch them off
  • Usage Statistics and Market Share of WordPress, W3Techs, figures as published on September 22, 2026, for the quoted “WordPress is used by 58.8% of all the websites whose content management system we know. This is 40.2% of all websites.”
  • WordPress 7.0.3 Released: 12 Vulnerabilities Found and Fixed, Patchstack, for the August 6, 2026 release date and the Anthropic credit for the Author-level safe CSS attribute filter bypass
  • WordPress Comment2Shell Flaw Can Turn Anonymous Comment XSS Into RCE via Admin Session, Swati Khandelwal, The Hacker News, September 22, 2026, cited for the Comment2Shell nickname and for the current absence of any report of exploitation in the wild or any CISA KEV listing