WP Statistics <= 14.16.4 - Unauthenticated Stored Cross-Site Scripting via 'utm_source' Parameter
Description
The WP Statistics plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'utm_source' parameter in all versions up to, and including, 14.16.4. This is due to insufficient input sanitization and output escaping. The plugin's referral parser copies the raw utm_source value into the source_name field when a wildcard channel domain matches, and the chart renderer later inserts this value into legend markup via innerHTML without escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in admin pages that will execute whenever an administrator accesses the Referrals Overview or Social Media analytics pages.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=14.16.4What Changed in the Fix
Changes introduced in v14.16.5
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-5231 (WP Statistics) ## 1. Vulnerability Summary The **WP Statistics** plugin for WordPress is vulnerable to **Unauthenticated Stored Cross-Site Scripting (XSS)** via the `utm_source` parameter. The vulnerability exists because the plugin's referral parser ex…
Show full research plan
Exploitation Research Plan - CVE-2026-5231 (WP Statistics)
1. Vulnerability Summary
The WP Statistics plugin for WordPress is vulnerable to Unauthenticated Stored Cross-Site Scripting (XSS) via the utm_source parameter. The vulnerability exists because the plugin's referral parser extracts values from the utm_source query parameter and stores them unsanitized in the database (specifically the source_name field) when certain wildcard conditions are met. Later, when an administrator views statistics charts (like Referrals or Social Media analytics), the plugin's JavaScript charting component renders these values using the innerHTML property without prior escaping, allowing for arbitrary script execution in the context of the administrator's session.
2. Attack Vector Analysis
- Endpoint: The tracking endpoint used by the plugin to record visitor hits. This is typically found at
wp-json/wp-statistics/v2/hitorwp-admin/admin-ajax.php(if ad-blocker bypass is enabled). - Vulnerable Parameter:
utm_source(passed via the URL of the page being tracked). - Authentication: Unauthenticated (any visitor can trigger a tracking hit).
- Preconditions:
- The plugin must be active and tracking visitors.
- The
utm_sourcemust be processed by the "referral parser." This often requires the visitor to have aRefererheader or for the URL to match a specific "channel" logic.
- Vulnerable Sink (Client-side):
assets/dev/javascript/chart.jsinsideexternalTooltipHandler.assets/dev/javascript/components/traffic-hour-chart.jsinsidehourTooltipHandler.- Both functions concatenate
dataset.labelinto an HTML string and set it viatooltipEl.innerHTML.
3. Code Flow
- Ingestion (Unauthenticated):
- A visitor accesses a page:
https://victim.com/?utm_source=<img src=x onerror=alert(1)>. assets/js/tracker.jsexecutesWpStatisticsUserTracker.init().sendHitRequest()is called, which gathers parameters includingpage_uri(Base64 encoded current URL).- The hit request is sent to the server.
- A visitor accesses a page:
- Storage (Server-side):
- The plugin receives the hit.
- It decodes
page_uriand parses the query string. - The referral parser identifies the
utm_source. - The raw, unsanitized value is saved into the database (field
source_name).
- Execution (Authenticated Admin):
- Admin visits
wp-admin/admin.php?page=wps_referrals_page(Referrals Overview). - The page loads chart data via an internal AJAX/REST call.
- The chart data includes the malicious
source_nameas a label for a dataset. assets/dev/javascript/components/traffic-hour-chart.jstriggershourTooltipHandlerwhen the admin hovers over the chart.innerHtml += ... ${dataset.label} ...(line 51) includes the payload.tooltipEl.innerHTML = innerHtml;(line 92) executes the script.
- Admin visits
4. Nonce Acquisition Strategy
Tracking hits in WP Statistics often require a nonce found in the WP_Statistics_Tracker_Object.
- Shortcode Identification: The plugin typically enqueues the tracker on all public pages if tracking is enabled.
- Extraction:
- Use
browser_navigateto the homepage. - Use
browser_evalto extract the configuration object:JSON.stringify(window.WP_Statistics_Tracker_Object)
- Use
- Key Components:
- Endpoint:
WP_Statistics_Tracker_Object.hitParams.endpoint - Nonce:
WP_Statistics_Tracker_Object.hitParams.wp_nonce(if present) or checked via the REST API headers. - Rest URL:
WP_Statistics_Tracker_Object.requestUrl
- Endpoint:
5. Exploitation Strategy
- Visit Site with Payload: Navigate to the site with the payload in the URL. This ensures the tracker picks it up naturally.
- URL:
http://localhost:8080/?utm_source=<img src=x onerror=alert(window.origin)>
- URL:
- Manual Hit Trigger (Backup): If the automatic hit doesn't trigger, manually send the POST request to the tracking endpoint.
- Method: POST
- URL: From
WP_Statistics_Tracker_Object.requestUrl+/+WP_Statistics_Tracker_Object.hitParams.endpoint - Headers:
Content-Type: application/x-www-form-urlencoded - Body (URL Encoded):
referred:http://www.google.com(to trigger referral parser)page_uri: Base64 of/?utm_source=<img src=x onerror=alert(window.origin)>- Include all other keys from
WP_Statistics_Tracker_Object.hitParams.
- Trigger Admin Execution:
- Log in as an administrator.
- Navigate to the Referrals page:
/wp-admin/admin.php?page=wps_referrals_page. - Hover over the chart elements to trigger the tooltip containing the
innerHTMLsink.
6. Test Data Setup
- Plugin Configuration: Ensure WP Statistics is installed and activated.
- Public Content: A standard post or page must exist (the default "Hello World" is sufficient).
- Referral Source: To ensure the "wildcard channel" matches, it is recommended to set a
Refererheader likehttps://www.google.comorhttps://twitter.comduring the hit request.
7. Expected Results
- The tracking hit will be successfully recorded.
- The payload
<img src=x onerror=alert(window.origin)>will appear unescaped in the database. - When an administrator views the Referrals chart and hovers over the malicious data point, a JavaScript alert showing the site's origin will appear.
8. Verification Steps
- Database Check: Run a WP-CLI command to verify the payload is stored:
wp db query "SELECT * FROM wp_statistics_visitor WHERE last_counter LIKE '%utm_source%';" # Or check the referrals table wp db query "SELECT source_name FROM wp_statistics_search WHERE source_name LIKE '%<img%';" - DOM Check: Using
browser_evalwhile on the admin referrals page:- Check if the chart data contains the payload:
// Check Chart.js instances for the payload in labels Chart.instances[0].data.datasets.map(d => d.label)
- Check if the chart data contains the payload:
9. Alternative Approaches
- Social Media Channel: Try the payload via
utm_source=facebookand put the payload in theutm_mediumorutm_campaignifutm_sourceis filtered (though the CVE specifically points toutm_source). - Direct REST API Injection: If the tracker logic is complex, send the POST request directly to
wp-json/wp-statistics/v2/hitwith thepage_uriparameter containing the URL-encoded payload. - Bypass Ad-Blocker Endpoint: If the REST API is restricted, use
admin-ajax.php?action=wp_statistics_hit.
Summary
The WP Statistics plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting via the 'utm_source' parameter. This occurs because the plugin's referral parser fails to sanitize input stored in the 'source_name' field, which is subsequently rendered in administrative charts using innerHTML without escaping.
Vulnerable Code
// assets/dev/javascript/chart.js around line 125 innerHtml += ` <div class="current-data"> <div> <span class="current-data__color" style="background-color: ${dataset.hoverPointBackgroundColor};"></span> ${dataset.label} </div> <span class="current-data__value">${value.toLocaleString()}</span> </div>`; --- // assets/dev/javascript/components/traffic-hour-chart.js around line 44 innerHtml += ` <div class="current-data"> <div> <span class="current-data__color" style="background-color: ${dataset.backgroundColor};"></span> ${dataset.label} </div> <span class="current-data__value">${value.toLocaleString()}</span> </div>`; --- // assets/dev/javascript/helper.js around line 348 let labelDiv = document.createElement('div'); labelDiv.innerHTML = labels[i]; labelDiv.setAttribute('aria-label', labels[i]);
Security Fix
@@ -125,7 +125,7 @@ <div class="current-data"> <div> <span class="current-data__color" style="background-color: ${dataset.hoverPointBackgroundColor};"></span> - ${dataset.label} + ${wps_js.escapeHtml(dataset.label)} </div> <span class="current-data__value">${value.toLocaleString()}</span> </div>`; @@ -496,7 +496,7 @@ </div>` : ''; legendItem.innerHTML = ` - <span>${dataset.label}</span> + <span>${wps_js.escapeHtml(dataset.label)}</span> <div> <div class="current-data"> <span class="wps-postbox-chart--item--color" style="border-color: ${dataset.borderColor}"></span> @@ -44,7 +44,7 @@ <div class="current-data"> <div> <span class="current-data__color" style="background-color: ${dataset.backgroundColor};"></span> - ${dataset.label} + ${wps_js.escapeHtml(dataset.label)} </div> <span class="current-data__value">${value.toLocaleString()}</span> </div>`; @@ -272,7 +272,7 @@ // Build the legend item HTML legendItem.innerHTML = ` <span class="current-data"> - <span class="wps-postbox-chart--item--color" style="border-color: ${dataset.backgroundColor}"></span> ${dataset.label} + <span class="wps-postbox-chart--item--color" style="border-color: ${dataset.backgroundColor}"></span> ${wps_js.escapeHtml(dataset.label)} </span> `; @@ -1,6 +1,12 @@ /* Start Wp-statistics Admin Js */ var wps_js = {}; +var _htmlEscapeMap = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}; +wps_js.escapeHtml = function (str) { + if (typeof str !== 'string') return str == null ? '' : String(str); + return str.replace(/[&<>"']/g, function (c) { return _htmlEscapeMap[c]; }); +}; + /* Get WP Statistics global Data From Frontend */ wps_js.global = (typeof wps_global != 'undefined') ? wps_global : []; Only in /home/deploy/wp-safety.org/data/plugin-versions/wp-statistics/14.16.4/assets/dev/javascript: Tinymce @@ -345,7 +345,7 @@ labelImageDiv.appendChild(img); } let labelDiv = document.createElement('div'); - labelDiv.innerHTML = labels[i]; + labelDiv.innerHTML = wps_js.escapeHtml(labels[i]); labelDiv.setAttribute('aria-label', labels[i]); labelDiv.classList.add('wps-horizontal-bar__label'); labelImageDiv.appendChild(labelDiv);
Exploit Outline
The exploit is unauthenticated and can be triggered by a visitor navigating to the target site with a malicious payload in the 'utm_source' query parameter (e.g., `/?utm_source=<img src=x onerror=alert(1)>`). The plugin's client-side tracker sends this URL to the tracking endpoint (usually `wp-json/wp-statistics/v2/hit`), where the server-side referral parser stores the raw payload in the database. When a site administrator views the analytics dashboard (specifically the Referrals or Social Media pages), the malicious script is rendered via an insecure `innerHTML` call in the charting component, executing the payload in the admin's session context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.