Research · 15 min read

Unauthenticated Stored XSS in Plugins Is Up 6x Since May

By WP Vanguard Team

Unauthenticated Stored XSS in Plugins Is Up 6x Since May

In May 2026, our database logged unauthenticated stored XSS disclosures rated high or critical at a rate of about 5 a week. Over the ten weeks since late June, it has averaged roughly 32 a week. Two of those weeks dipped, but the average sits at six times the May rate, and it isn't coming back down.

We track this because unauthenticated stored XSS is one of the few WordPress bug classes where the attacker only needs somewhere public to type, like a comment box or a form field. Someone submits content, WordPress's own sanitizer signs off on it, and it detonates later in an administrator's browser. The count going up is one signal. What's actually breaking underneath it is the more useful one, and it's the same failure showing up in plugin after plugin: content that passed wp_kses gets decoded or re-parsed somewhere downstream, after the sanitizer already trusted it.

6x

the May rate: unauthenticated stored XSS now averages about 32 high or critical disclosures a week, up from about 5

WP Vanguard vulnerability database, May to early October.

The weekly numbers

Here's the run from the start of May through early September, counting high/critical disclosures whose advisory title identifies them as unauthenticated XSS, against all disclosures logged that week.

Week starting Unauth XSS (high/critical) All disclosures
May 4 5 85
May 11 5 134
May 18 5 107
May 25 15 281
Jun 1 18 175
Jun 8 9 121
Jun 15 7 186
Jun 22 19 249
Jun 29 42 291
Jul 6 50 318
Jul 13 10 111
Jul 20 14 269
Jul 27 29 302
Aug 3 35 302
Aug 10 26 266
Aug 17 48 276
Aug 24 31 271
Aug 31 37 280

Across the four May weeks, unauthenticated XSS ran about 5 a week, 4 to 6 percent of everything disclosed. From Jun 29 through Sep 6, ten weeks, the total is 322, averaging just over 32 a week and 11 to 13 percent of all disclosures in that window. Between August 25 and September 10 alone, we logged 76 of these disclosures across 70 different plugins.

We can't pin the rise on one cause, but the timing matches a change on the finding side. Researchers started running AI models over plugin code in early 2026, and bug bounty programs felt it. Patchstack removed contributor-level reports from its bounty scope in June, citing a jump in lower-impact reports since AI-assisted research took off, and WordPress.org narrowed its own program in September after what it called a substantial increase in incoming reports.

Our read is that more of these bugs are being found, not that far more are being written. We look at the whole picture, and at what to do about it, in why WordPress vulnerabilities keep rising.

Update: the latest weeks (October 4)

The count still hasn't come down. Here are the four weeks since the first table, using the same method. The week of September 28 is partial, through October 4, and it has already passed every week before it.

Week starting Unauth XSS (high/critical) All disclosures
Sep 7 19 265
Sep 14 24 365
Sep 21 37 323
Sep 28 (partial) 46 200

That's 126 unauthenticated XSS disclosures in four weeks. Across the fourteen weeks since June 29, the average is still about 32 a week. Counts for earlier weeks creep up slightly as late-arriving disclosures get added under their original dates, so these figures differ a little from the first table, and the partial week will keep rising.

Since September 25 alone we've logged 47 of these across 45 plugins, and 14 of those plugins have 100,000 or more installs. All of them have a fixed version, and every one scores 7.2. These are the largest:

Plugin Active installs Vulnerable up to Fixed in Where the payload goes in
W3 Total Cache 900,000 2.10.6 2.10.7 Comment content
Autoptimize 800,000 3.1.15.1 3.1.16 The REQUEST_URI path
Forminator 600,000 1.57.2 1.57.2.1 A rich-text textarea field and the postdata-1[post-custom] parameter (two CVEs)
Ninja Forms 500,000 3.15.4 3.15.5 A paragraph text (rich text) field submission
Extendify 500,000 3.1.6 3.2.0 The styles.blocks block type key
SEOPress 300,000 10.2 10.3 Author display name
PDF Invoices & Packing Slips for WooCommerce 300,000 5.16.1 5.16.2 Billing first name, last name and company fields
CMB2 300,000 2.13.1 2.13.2 The file_list and textarea_code field types (two CVEs; 2.13.0 and earlier for the first)
Ultimate Member 200,000 2.13.1 2.14.0 The form_id parameter
Newsletter 200,000 9.4.0 9.4.1 The np1 custom field parameter
MW WP Form 200,000 5.1.7 5.1.8 The post_id parameter, through stored form-submitted post meta
Smash Balloon Social Post Feed 200,000 4.13.0 4.14.0 A Facebook comment message in the admin builder preview
Relevanssi 100,000 4.28.3 4.28.4 Comment content
Real Cookie Banner 100,000 5.3.5 5.3.6 Comments

Forminator and CMB2 each appear with two separate CVEs, and one update covers both. Forminator and Ninja Forms are form builders, which have a repeat-patching pattern of their own, covered in our look at form builder plugin vulnerabilities.

The shape is familiar. W3 Total Cache, Relevanssi and Real Cookie Banner all take the payload through comments, and SEOPress and PDF Invoices take it through ordinary name and billing fields that an administrator later sees in a screen of their own. Plain user-supplied text reaching an output nobody re-escaped is the same root problem as the ProfilePress shortcode injection, where profile fields kept leaking until the fix moved to output. The fixed versions are in our weekly patch list.

Key finding

The count hasn't come down. The last four weeks added 126 unauthenticated stored XSS disclosures, and the week of September 28 had passed every earlier week before it was over.

The pattern underneath the count

What ties these disclosures together isn't a shared plugin or a shared developer. It's a shared shape of bug.

WordPress's wp_kses function is the standard defense against stored XSS. It walks HTML, strips tags and attributes that aren't on an allowlist, and is supposed to leave behind only content that's safe to store and later render. It works well at the one job it's built for: filtering HTML at the moment content is saved.

The problem is what happens after. Several of the plugins in this dataset take kses-approved content and run it through a second step later, somewhere the sanitizer never sees. In W3 Total Cache it's a LazyLoad mutator processing comment content. TranslatePress runs comment HTML through its own parser. Gravity Forms' tooltip script drops an already-decoded value into innerHTML in the browser.

Each of those transformations is reasonable on its own. None of them re-run kses afterward. The content was safe when it was saved. It stops being safe the moment something downstream treats it as raw HTML again, and by then there's no sanitizer standing between the payload and the page.

Case studies from the recent batch

A sample of what's been disclosed and fixed in this window, with the vector and the condition needed to trigger it.

Plugin Vulnerable up to Fixed in Vector Condition
LiteSpeed Cache 7.8.1 7.9 Comment built from decimal numeric character references inside an allowed tag like <code> None stated in the advisory
W3 Total Cache 2.10.5 2.10.6 LazyLoad background image mutator rewriting comment content Lazy Load Images on, with Process background images enabled
TranslatePress 3.3.3 3.3.4 Comment noise-key injection into its HTML parser via an anchor tag's href/title Anchor structure is inside WordPress's own comment kses allowlist
Gravity Forms 2.10.5 3.0.0 Post Body field value, re-parsed client-side into a tooltip's innerHTML Payload survives wp_kses_post because the tags/attributes are allowed
WP Fastest Cache 1.5.0 1.5.1 HTTP Host header Polylang or Polylang Pro active, Combine JS enabled
Broken Link Checker 2.4.13 2.4.13.1 Comment author URL and link log Admin runs the dismiss-and-recheck workflow on the attacker's link
WP Rocket 3.21.0.1 3.21.1 rocket_beacon AJAX endpoint None specified

The LiteSpeed Cache case (CVE-2026-18978) is the cleanest illustration of the core problem. A comment built entirely from decimal numeric character references, things like &#34;, &#60;, &#62;, placed inside an element kses already allows, such as <code>, sails through sanitization. Nothing about that comment looks like a tag when kses inspects it. It only becomes one after something decodes those entities back into characters.

TranslatePress (CVE-2026-76053) shows a different flavor of the same idea. The payload is an anchor tag with an href and a title attribute, a structure that's explicitly permitted in WordPress's own comment kses allowlist. It's not smuggled past the filter. It's built entirely from pieces the filter was told to trust. The bug lives in what TranslatePress's own HTML parser does with that anchor afterward, not in anything kses missed.

Gravity Forms (CVE-2026-16649) moves the failure to the browser. The Post Body field survives save-time sanitization because wp_kses_post allows the tags and attributes involved, that part is working as designed. The break happens later, when a client-side tooltip script takes the already-decoded aria-label value and re-parses it as innerHTML. Sanitization ran once, correctly, at save time. Nothing ran a second time before the value hit the DOM.

W3 Total Cache (CVE-2026-78438) and WP Fastest Cache (CVE-2026-19760) both need a specific feature flag flipped before the path is reachable at all, LazyLoad's background-image processing in one case, Polylang plus Combine JS in the other. Broken Link Checker (CVE-2026-75528) needs an administrator to actively run a review workflow on the attacker's submitted link. Those conditions narrow the exposure. They don't remove it.

Several more plugins landed fixes in the same window without needing a deep dive here: EWWW Image Optimizer (fixed 8.7.7), Breadcrumb NavXT (7.5.2), Ninja Forms (3.15.2), SureForms (2.12.3), CleanTalk Spam Protection (6.87), Forminator (1.57.0.2 and later), Under Construction (5.83), Optimole (4.2.11), and iubenda (3.13.5). Different plugins, different maintainers, same underlying bug class.

Three more landed on September 17, in plugins with a combined install base of about two million sites. Complianz GDPR/CCPA Cookie Consent (CVE-2026-83561, fixed in 7.5.5) is the one to notice. Its payload rides through the Elementor cookie blocker's regex, which rewrites stored content after sanitization has already happened, so it's the same downstream-transform shape again. Popup Maker (CVE-2026-87915, fixed in 1.25.0) takes it through a form's values[Name] parameter. Jeg Kit for Elementor (CVE-2026-18405, fixed in 3.2.17) takes it through comment content. All three are unauthenticated and scored 7.2.

Why the severity isn't a straight 9.8

If unauthenticated stored XSS sounds like it should score as high as the Super Forms file upload flaw we wrote about in July, most of the time it doesn't, and that's worth being honest about.

CVSS scores for these disclosures typically land around 7.2, not 9.8. The reason is the "unauthenticated" part is only half the story. Many of these bugs need a feature toggle flipped that isn't on by default, a comment or link that has to clear moderation first, or an administrator to take a specific action like running a link-check workflow. Take away the "requires nothing" framing and what's left is still serious, but it's conditional in a way a raw file-upload RCE isn't.

That conditionality is real. It's also easy to overstate in the other direction. A default-off setting that a meaningful share of sites actually run, or a moderation queue that gets approved routinely because most comments are legitimate, isn't much of a barrier in practice. The score reflects the requirement existing. It doesn't tell you how often that requirement is actually met on real sites.

Why it still matters once it fires

Stored XSS that executes in an administrator's browser runs with that administrator's session. Nothing about the payload needs privilege escalation, because it's already running as a privileged user by the time it executes.

From inside that session, a script can do anything the admin UI or the REST API lets an administrator do: create a new user with administrator role, change site settings, install a plugin. Two-factor authentication doesn't help here. 2FA protects the login step. Once the admin is already logged in and the payload fires inside their active session, the authentication check already passed, and there's no second factor left to check.

This is a fundamentally different attack shape than most of the ways WordPress sites actually get compromised, which usually involve a vulnerable endpoint an attacker hits directly. Stored XSS waits. It sits in a comment or a form submission until the right person happens to view it, then acts through their credentials instead of around them.

What site owners should do

Update the plugins named above if you're running them, and put Complianz, Popup Maker and Jeg Kit at the top of that list, since they're the newest and have the largest install bases. If you can't update immediately, disabling the specific feature named in the condition column, LazyLoad's background processing, Combine JS, buys you time without a full patch.

Beyond the named plugins, this is a good moment to prune ones that touch comments, forms, or content output but you don't actually use. Every plugin in this batch that processes comment or form content is a plugin with a re-parsing path an attacker can target. Fewer of them means fewer paths.

Keep comment moderation limited to a small number of trusted accounts, and be skeptical of "just approve it, it looks fine" as a review standard, since several of these bugs need exactly that approval step to fire. A Content-Security-Policy header that restricts script sources won't stop every variant, but it meaningfully limits what a successful payload can actually do once it executes. Our WordPress security checklist covers the rest of the baseline hardening worth having in place.

What developers should do

Escape on output, every time, regardless of what already happened to the content on the way in. Input sanitization answers "was this safe when it was saved." Output escaping answers "is this safe right where it's about to render," and those are different questions with different answers depending on context.

Never re-parse stored content into innerHTML or decode it back from HTML entities without running it through sanitization again first. The Gravity Forms and LiteSpeed Cache cases above are both variations on exactly this mistake: content that was safe once, then treated as trusted a second time somewhere the original sanitizer never touched.

Treat any kses-approved value as untrusted again the instant you transform it, whether that transformation is a cache rewrite, a translation pass, a lazy-load mutator, or a client-side script reading a decoded attribute. wp_kses clearing your content once is not a permanent guarantee. It's a snapshot of what was safe at that moment, for that specific rendering path. For more on how sanitization interacts with the request-level checks that get confused for it, see our piece on what WordPress nonces protect and what they don't; it's a related mistake, treating one specific check as if it covers every security question at once.

Still unpatched as of October 4

As of October 4, 2026, our database has no fixed version on record for these unauthenticated XSS entries: wp-quicklatex, ninja-forms-uploads, quick-event-manager, quickcal, easy-appointments and sidebar-manager-light. estatik is on the same list with a caveat: the plugin has shipped 4.3.5 and 4.3.6 since its stored XSS (CVE-2026-81775, version 4.3.4 and earlier) was disclosed, so check its changelog before assuming it's still open. One plugin has dropped off since our September 18 list: TrustedSite now has a fix in 1.2.6.

If you're running any of the others, that's a plugin worth watching closely, or disabling the affected feature, until a fix ships. We'll update our data as patches land; see our August 2026 vulnerability roundup for how we track this month over month, and our broader look at where WordPress's vulnerability numbers actually come from for the context behind why plugins carry almost all of this weight.

Check your own site

WP Vanguard's surface scan checks your site's security headers and matches every detected plugin version against our vulnerability database, including the disclosures in this post. If one of the plugins above is running at a vulnerable version and the scan can detect that version, it gets flagged. Run a free scan to see where you stand.

References

unauthenticated-stored-xss cross-site-scripting wp-kses wordpress-plugin-vulnerabilities vulnerability-trends

Related reading

Check Your WordPress Site Security

Free scan, no login required. Find vulnerabilities before attackers do.

Scan Your Site Free

Get weekly WordPress security tips

Vulnerability alerts, plugin updates, and security guides. No spam. Unsubscribe any time.

WP Vanguard is built by Wbcom Designs, makers of Reign, Jetonomy, Listora, and more. Explore our WordPress products →
← Back to Blog