WP Statistics – Simple, privacy-friendly Google Analytics alternative <= 14.16.6 - Unauthenticated Stored Cross-Site Scripting
Description
The WP Statistics – Simple, privacy-friendly Google Analytics alternative plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 14.16.6 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v14.16.7
Source Code
WordPress.org SVNThis research plan outlines the steps to investigate and exploit a Stored Cross-Site Scripting (XSS) vulnerability in the **WP Statistics** plugin (versions <= 14.16.6). --- ### 1. Vulnerability Summary * **Vulnerability:** Unauthenticated Stored Cross-Site Scripting (XSS). * **Location:** Vis…
Show full research plan
This research plan outlines the steps to investigate and exploit a Stored Cross-Site Scripting (XSS) vulnerability in the WP Statistics plugin (versions <= 14.16.6).
1. Vulnerability Summary
- Vulnerability: Unauthenticated Stored Cross-Site Scripting (XSS).
- Location: Visitor statistics recording logic and subsequent admin dashboard rendering.
- Cause: The plugin records visitor details (Browser, Platform, Device) based on the
User-Agentheader. These values are stored in the database and later displayed in the WordPress admin dashboard templates (e.g.,browsers.php,categories.php,platforms.php) without sufficient output escaping. - Affected Files:
includes/admin/templates/pages/devices/browsers.phpincludes/admin/templates/pages/devices/categories.phpincludes/admin/templates/pages/devices/platforms.phpincludes/api/v2/class-wp-statistics-api-hit.php(REST API hit endpoint)
2. Attack Vector Analysis
- Endpoint: Either the main site frontend (triggering automatic tracking) or the REST API endpoint
/wp-json/wp-statistics/v2/hit. - Payload Location: The
User-AgentHTTP request header. - Authentication: None (Unauthenticated).
- Preconditions:
- The plugin must be active and configured to track visits.
- For the REST API attack: The "Cache Plugin" setting (
use_cache_plugin) must be enabled.
3. Code Flow
- Entry Point (REST):
WP_STATISTICS\Api\v2\Hit::hit_callback()is triggered via aPOSTrequest towp-json/wp-statistics/v2/hit. - Processing:
hit_callback()callsWP_STATISTICS\Hits::record(). - Storage:
Hits::record()extracts theUser-Agentfrom the environment, uses a parser (likelyDeviceHelper) to determine the browser/platform/device, and stores these strings in the database (typically the{prefix}statistics_visitortable). - Admin Rendering: An administrator navigates to WP Statistics > Optimization or specific device reports.
- Sink: The template
includes/admin/templates/pages/devices/browsers.php(and others) iterates through the stored visitors. - XSS Trigger: The code executes:
The function<span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>" ...> ... <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?> </span>unknownToNotSet()merely returns the string if it is not "unknown". Since there is noesc_html()oresc_attr()surrounding the echo, the payload executes in the admin's browser.
4. Nonce Acquisition Strategy
If attacking via the REST API (hit endpoint), a signature check is performed:
- Action String: The REST route uses
checkSignature($request). - Acquisition:
- Identify a page where WP Statistics tracking is active (usually all public pages).
- Use
browser_navigateto visit the homepage. - WP Statistics typically localizes data for its frontend script. Use
browser_evalto look for variables likewp_statistics_trackerorWP_Statistics_L10n. - Specific to REST: Check
window.WP_Statistics_REST_Config?.nonceor similar.
- Fallback: The vulnerability is "unauthenticated" because simple visits to the homepage with a spoofed
User-Agentare recorded by the plugin's standard hook intowportemplate_redirectif the REST hit API is not being used.
5. Exploitation Strategy
Method A: Direct Visit (Recommended)
- Payload:
Mozilla/5.0 (X11; Linux x86_64) <script>alert(document.domain)</script> - Request: Use
http_requestto send aGETrequest to the WordPress homepage. - Headers: Set the
User-Agentheader to the payload above. - Wait: The plugin may record hits asynchronously or on the next tick; usually, it is immediate.
Method B: REST API (If Method A fails)
- Endpoint:
POST /wp-json/wp-statistics/v2/hit - Headers:
User-Agent:"><img src=x onerror=alert(1)>Content-Type:application/x-www-form-urlencoded
- Body Parameters:
page_uri:/_wpnonce: (Acquired from JS context if signature check is enforced)
6. Test Data Setup
- Install Plugin:
wp plugin install wp-statistics --version=14.16.6 --activate - Enable Tracking: Ensure tracking is enabled (default behavior).
- Optional: To test the REST hit path, enable the cache setting:
wp option patch update wp_statistics_options use_cache_plugin 1
7. Expected Results
- The
http_requestshould return a200 OK(for Method A) or a JSON success response (for Method B). - When an administrator views the "Browsers" or "Devices" report, the injected script will execute.
8. Verification Steps
- Check Database: Use WP-CLI to verify the payload is stored:
wp db query "SELECT agent FROM $(wp db prefix)statistics_visitor ORDER BY ID DESC LIMIT 1;" - Confirm Sink: Navigate to the browser report page as an admin:
browser_navigate("http://localhost:8080/wp-admin/admin.php?page=wp-statistics-browsers") - Verify Execution: Use
browser_evalto check for a global flag set by your payload (if using a more complex payload thanalert).
9. Alternative Approaches
- Platform Payload: If the browser parser filters certain characters, try targeting the Platform field by crafting a User-Agent that triggers a specific OS detection followed by the payload.
- Device Category: Target
categories.phpby crafting a User-Agent that mimics a mobile device with an injected string. - Search Engine XSS: Check if the "Search Engines" or "Referrals" reports are also vulnerable by spoofing the
Refererheader with an XSS payload in the query parameters of a search engine URL.
Summary
The WP Statistics plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting (XSS) due to a failure to sanitize and escape visitor information derived from the User-Agent header. An attacker can inject malicious scripts by visiting the site with a crafted User-Agent, which is then executed in the context of an administrator's browser when they view visitor reports.
Vulnerable Code
// includes/admin/templates/pages/devices/browsers.php lines 39-42 <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>" class="wps-browser-name"> <img alt="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>" src="<?php echo esc_url(DeviceHelper::getBrowserLogo($item->agent)); ?>" class="log-tools wps-flag"/> <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?> </span> --- // includes/admin/templates/pages/devices/categories.php lines 35-37 <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->device); ?>" class="wps-model-name"> <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->device); ?> </span> --- // includes/admin/templates/pages/devices/platforms.php lines 36-39 <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?>" class="wps-platform-name"> <img alt="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?>" src="<?php echo esc_url(DeviceHelper::getPlatformLogo($item->platform)); ?>" class="log-tools wps-flag"/> <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?> </span>
Security Fix
@@ -36,9 +36,9 @@ <?php foreach ($data['visitors'] as $item) : ?> <tr> <td class="wps-pd-l"> - <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>" class="wps-browser-name"> - <img alt="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?>" src="<?php echo esc_url(DeviceHelper::getBrowserLogo($item->agent)); ?>" class="log-tools wps-flag"/> - <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent); ?> + <span title="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent)); ?>" class="wps-browser-name"> + <img alt="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent)); ?>" src="<?php echo esc_url(DeviceHelper::getBrowserLogo($item->agent)); ?>" class="log-tools wps-flag"/> + <?php echo esc_html(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->agent)); ?> </span> </td> <td class="wps-pd-l"> @@ -32,8 +32,8 @@ <?php foreach ($data['visitors'] as $item) : ?> <tr> <td class="wps-pd-l"> - <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->device); ?>" class="wps-model-name"> - <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->device); ?> + <span title="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->device)); ?>" class="wps-model-name"> + <?php echo esc_html(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->device)); ?> </span> </td> <td class="wps-pd-l"> @@ -33,9 +33,9 @@ <?php foreach ($data['visitors'] as $item) : ?> <tr> <td class="wps-pd-l"> - <span title="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?>" class="wps-platform-name"> - <img alt="<?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?>" src="<?php echo esc_url(DeviceHelper::getPlatformLogo($item->platform)); ?>" class="log-tools wps-flag"/> - <?php echo \WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform); ?> + <span title="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform)); ?>" class="wps-platform-name"> + <img alt="<?php echo esc_attr(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform)); ?>" src="<?php echo esc_url(DeviceHelper::getPlatformLogo($item->platform)); ?>" class="log-tools wps-flag"/> + <?php echo esc_html(\WP_STATISTICS\Admin_Template::unknownToNotSet($item->platform)); ?> </span> </td> <td class="wps-pd-l">
Exploit Outline
The exploit involves sending an unauthenticated HTTP request to the WordPress site (either to the homepage or the REST API hit endpoint) with a malicious User-Agent header. This payload, which contains JavaScript (e.g., using <script> or event handlers like onerror), is parsed by the plugin's device helper and stored in the database. The XSS triggers when an administrator navigates to the 'Browsers', 'Categories', or 'Platforms' report pages in the WP Statistics menu, as the plugin renders the stored agent/device/platform string directly into HTML attributes and content without escaping.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.