CVE-2026-94078 Unauthenticated stored XSS in Site Reviews
Intrudify's autonomous pentesting engine discovered a stored cross-site scripting vulnerability in Site Reviews, affecting all versions up to and including 8.3.1. Any unauthenticated visitor who can submit a review can store arbitrary HTML that renders unescaped on every page listing reviews. The payload does not go in a review value, because values are sanitised. It goes in a custom-field name, which the plugin never sanitises at all.
Summary
Site Reviews adds review collection and display to WordPress through public shortcodes. Intrudify's autonomous testing engine found that the plugin treats every unrecognised submission parameter as a custom field, stores it under a post meta key derived from the parameter name, and never sanitises that name.
Since the submission form is public and reviews are published immediately under default settings, a single anonymous POST is enough to put executing markup on every page that lists reviews. No account, no approval step and no interaction from anyone.
Technical detail
Every unknown parameter becomes a custom field
In plugin/Commands/CreateReview.php:
protected function custom(): array {
$fields = [];
foreach ($this->request->toArray() as $key => $value) {
$key = Str::removePrefix($key, 'custom_');
$fields[$key] = $value; // attacker-controlled KEY
}
return glsr(CustomFieldsDefaults::class)->filter($fields);
}The loop accepts anything in the request it does not recognise and uses the submitted parameter name as the field key.
The sanitiser is built from the thing it should be sanitising
This is the part worth pausing on. CustomFieldsDefaults constructs its sanitiser map from those keys. It maps each key to a rule for cleaning that key's value, so by construction it can only ever sanitise values. The names are the index of the map, not entries in it, which puts them structurally outside anything the filter can reach.
A reviewer looking at this code sees a sanitiser being applied to the custom fields and reasonably concludes the custom fields are sanitised. They are, on one axis. The other axis was never in scope.
Where the name reaches the page
The name is later concatenated into the review HTML through Cast::toString(..., $strict = false), which falls through to maybe_serialize(). That happens after all output filtering has run: the template engine substitutes context keys one at a time, and content is substituted before custom, so the custom data lands in already-filtered output.
The rendered result carries the serialised array verbatim, payload and all:
<p>a:1:{s:42:"<img src=x onerror=...>";s:1:"1";}The tag is parsed and the handler executes for every visitor who loads the page.
Why no approval step intervenes
Under Settings, General, the Require Approval option defaults to no, so a submitted review is published immediately. That removes the administrator from the chain entirely. Unlike a Contributor-level finding, nobody has to review anything for the payload to go live.
Verification and control
We verified this live against 8.3.1. The payload renders unescaped inside the review content and executes in a real browser on page load, with the submission made anonymously using only the hidden form fields that the public page serves to any visitor.
As a negative control, a review whose custom-field name is plain text renders as text with nothing executing, and payloads placed in field values are correctly sanitised. Only the name path is vulnerable, and we state it that way rather than describing the plugin's sanitisation as broken in general.
Impact
This is the widest-reaching class of stored XSS: no authentication, no privilege, no user interaction, and no administrator action required at any point. One anonymous POST to a public form places executing script on every front-end page that lists reviews.
Every visitor to those pages runs the payload in the site's origin. That includes logged-in administrators browsing the front end, which puts session-scoped administrative actions in reach, and ordinary visitors, whose sessions and submitted data are exposed. Because review listings are typically placed on high-traffic commercial pages, the exposed population is usually the site's whole audience rather than a single reviewer.
Remediation
Update Site Reviews to 8.3.2 or later.
If you cannot update immediately, enable Require Approval under Settings, General. That does not fix the defect, but it puts a human between submission and publication.
For maintainers, sanitise the key as well as the value when accepting arbitrary parameters as custom fields, ideally by restricting names to a safe character set rather than by escaping them. And escape at the point of output, so that data substituted into the template after filtering has run is not trusted simply because it arrived late.
The wider lesson
Sanitisation is usually reasoned about as a property of values, because values are what users are understood to control. Here the user controls the key space too, and the code that looks like the security control is keyed by the untrusted data. That arrangement cannot sanitise names no matter how thorough its value rules are, and it reads as safe on inspection precisely because a sanitiser is visibly being applied.
The second half is ordering. The template engine substitutes context keys one at a time, and output filtering had already run by the time the custom data was inserted. Anywhere a rendering pipeline filters and then continues substituting, the substitutions that happen afterwards are unprotected. Worth checking the order of operations in any template layer that mixes filtering with staged substitution.
Disclosure timeline
- 2026-09-18 Vulnerability identified and validated by Intrudify, reported via Patchstack
- 2026-09-24 Public disclosure · CVE-2026-94078 assigned
Questions
What is CVE-2026-94078?
CVE-2026-94078 is an unauthenticated stored cross-site scripting vulnerability in the Site Reviews WordPress plugin, versions 8.3.1 and earlier. It was discovered autonomously by the Intrudify AI penetration testing engine. Any visitor who can submit a review can store arbitrary HTML in a custom-field name, which the plugin never sanitises, and it renders unescaped on every page that lists reviews.
Does CVE-2026-94078 require an account?
No. The review submission form is public, and reviews are published immediately under default settings because Require Approval defaults to off. No account, no administrator action and no user interaction are needed for the payload to become live.
How do I fix CVE-2026-94078?
Update Site Reviews to 8.3.2 or later. If you cannot update immediately, enable Require Approval under Settings, General, so submitted reviews are not published automatically.