The Exploit
An unauthenticated attacker can inject a malicious script into the utm_source parameter; on the next admin page load of the Referrals Overview or Social Media analytics, the payload executes in the administrator's browser with full dashboard privileges.
GET /index.php?utm_source=<img+src=x+onerror=alert(document.domain)> HTTP/1.1
Host: target.wordpress.local
When an administrator later navigates to the Referrals Overview page in wp-admin, the injected <img> tag is rendered directly into the chart legend via innerHTML, triggering the onerror handler. The attacker observes no immediate response to the crafted request; the payload lies dormant in the analytics database until an admin visits the vulnerable page. At that moment, the JavaScript executes in the admin's session context, granting the attacker access to read CSRF tokens, modify posts, create admin accounts, or exfiltrate sensitive data visible only to administrators.
Why this still matters at admin: WP Statistics is installed on hundreds of thousands of WordPress sites. In multi-tenant or agency hosting environments, the attack surface expands: a restricted shop manager compromised via email phishing clicks a malicious referral link on an external site. The utm_source parameter persists in the database. When the site owner (full admin) next checks analytics, the payload fires. Alternatively, attackers can poison referral data via compromised ad networks or open redirects, ensuring the malicious utm_source is logged automatically without requiring the attacker to directly access the target site.
What the Patch Did
Before:
$channels[$key]['name'] = $value;
After:
$channels[$key]['name'] = sanitize_text_field($value);
The patch applies sanitize_text_field(), WordPress's core input sanitization function. This function strips HTML tags, removes line breaks, and encodes special characters (including <, >, and &) to their HTML entity equivalents. It is the standard WordPress API for sanitizing text destined for display in user-facing contexts where HTML markup is not permitted. By sanitizing at the point of ingestion—when the utm_source parameter is first assigned to the channel name—the plugin ensures that any downstream use of that value, regardless of output context, cannot inject executable scripts.
Root Cause
CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
The dataflow begins when a user (or ad network) embeds a utm_source query parameter in a referral URL. The WP Statistics plugin's ReferralsParser class captures this parameter and, when a wildcard channel domain matches, assigns the raw value directly to $channels[$key]['name'] without any sanitization. This unsanitized value is then persisted into the analytics database. Later, when the admin dashboard renders the Referrals Overview or Social Media pages, the chart-rendering code retrieves the stored name field and inserts it into HTML legend markup via innerHTML (not textContent). Because neither input sanitization nor output escaping was performed at either boundary, an attacker-controlled script tag embedded in utm_source traverses the trust boundary unchecked: from an external request parameter → into the database → into rendered HTML → into the browser DOM → into script execution context.
Why It Works
The load-bearing line in the patch is sanitize_text_field($value). Remove it, and the bug remains fully exploitable: the unsanitized value still reaches the database and still gets rendered into the chart legend. sanitize_text_field() is the only defense because it removes the syntactic building blocks of an XSS payload—the angle brackets and event handler attributes. The engineer added it at the ingestion point (inside ReferralsParser) rather than at the output point (inside the chart renderer) because input sanitization is simpler, more maintainable, and more resilient to future bugs: it ensures the data stored in the database is already safe, so no downstream code can accidentally re-expose the vulnerability via a different output method. This is defence-in-depth in practice: the sanitization check is placed at the earliest trust boundary, ensuring all subsequent uses of the data inherit the safety guarantee.
Hardening Checklist
-
Always sanitize external request parameters at ingestion. Use
sanitize_text_field()for plain text,sanitize_email()for email addresses, andsanitize_url()for URLs. Apply the appropriate function immediately when the parameter is first assigned, not later in the pipeline. -
Use output escaping as a second layer. Even if input is sanitized, apply context-aware escaping at render time:
esc_html()for HTML content,esc_attr()for HTML attributes,esc_js()for JavaScript strings. Never rely on sanitization alone. -
Audit all analytics and logging ingestion points. Query parameters, referrer headers, user-agent strings, and ad-network callbacks are common vectors for storing attacker-controlled data. Trace each field from source to database to display.
-
Never use
innerHTMLwith unsanitized or unescaped data. Replaceelement.innerHTML = $valuewithelement.textContent = $valuewhen possible, or construct DOM nodes viadocument.createElement()andappendChild(). If HTML markup is genuinely needed, use a DOM sanitization library like DOMPurify. -
Test referral injection as part of your security test suite. Add unit tests that verify payloads like
<img src=x>,"><script>alert(1)</script>, and event-handler attributes are stripped or escaped at both ingestion and output.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-5231