The Exploit
An unauthenticated attacker must be able to add or modify a review on a Google Place that is connected to the vulnerable WordPress site. Once the malicious review is stored in Google's system, the plugin will automatically import it during the next sync cycle.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.wordpress.local
Content-Type: application/x-www-form-urlencoded
action=trustindex_get_reviews&platform=google&place_id=ChIJ...&reviews=[{"review_text":"Great place! <img src=x>","reviewer":"Attacker","rating":5}]
When a WordPress admin or frontend visitor loads a page displaying imported Google Reviews, the injected <img src=x> payload executes in their browser context. The attacker observes script execution indicated by the JavaScript alert popup displaying the site domain. Stored XSS is confirmed when the payload persists across page reloads and affects all users viewing the reviews widget.
What the Patch Did
Before
return preg_replace('/\r\n|\r|\n/', "\n", trim(html_entity_decode($text, ENT_HTML5 | ENT_QUOTES)));
After
return wp_kses_post(preg_replace('/\r\n|\r|\n/', "\n", trim(html_entity_decode($text, ENT_HTML5 | ENT_QUOTES))));
The patch wraps the review text processing pipeline with wp_kses_post(), WordPress's native HTML allowlist sanitization function. This function strips dangerous HTML tags and attributes while preserving safe formatting tags like <p>, <br>, and <strong>. The security control added is output escaping via allowlist-based HTML sanitization — the wp_kses_post() API specifically designed to prevent stored XSS in user-generated content that must retain some HTML structure.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting).
Imported Google Review text enters the plugin via the AJAX endpoint trustindex_get_reviews parameter reviews, which contains JSON-serialized review objects. The review text field flows through html_entity_decode() in the processing function at line 6020 of trustindex-plugin.class.php. The html_entity_decode() call is the critical mistake: it reverses HTML entity encoding, converting <img src=x> back into the raw <img src=x> tag. The output is then returned directly to the template layer without any sanitization, crossing the trust boundary between untrusted external data (Google reviews) and the browser's HTML parser. An attacker who controls a review on a linked Google Place can embed malicious HTML that will be decoded and rendered verbatim.
Why It Works
The single load-bearing line in the patch is wp_kses_post(...). This function inspects the decoded HTML token stream and rejects any tag or attribute not in WordPress's whitelist of safe formatting elements. Removing this wrapper would leave the decoded malicious HTML intact — the earlier html_entity_decode() would still reverse entity encoding, and the output would still reach the browser unfiltered. The preg_replace() and trim() calls are defensive helpers that normalize newlines and whitespace, but they perform no security function; they cannot stop attribute-based payloads like <img onerror> or <svg onload>. The engineer added them for content presentation, not for exploitation prevention. Only wp_kses_post() actually blocks the attack because it understands HTML grammar and WordPress's security policy: "allow safe tags, reject everything else."
Hardening Checklist
-
Sanitize all external review/user content at output time using
wp_kses_post(), not just at import. Even if a future plugin update introduces another unfiltered sink, allowlist HTML sanitization at the display layer provides defense-in-depth. -
Never call
html_entity_decode()followed by direct output. If you must reverse entity encoding for content processing, immediately pass the result throughwp_kses_post(),esc_html(), or equivalent. Treathtml_entity_decode()as a dangerous operation that requires post-processing. -
Audit all AJAX endpoints that return user-generated or externally-sourced content. Search for patterns:
$_POST['reviews'],$_GET['data'], API responses — and verify that each one is escaped for its output context using the appropriate WordPress escaping function (esc_html(),esc_attr(),wp_kses_post(), etc.). -
Use WordPress's REST API with schema validation instead of custom AJAX handlers for importing external data. The REST API's built-in validation and escaping middleware reduces the surface area for these errors.
-
Run automated security linting on plugin code using tools like PHPStan with WordPress rulesets or Semgrep rules for CWE-79. These tools flag
html_entity_decode()followed by direct output and catch missingwp_kses_*()calls.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-12510