Slimstat Analytics <= 5.3.2 - Reflected Cross-Site Scripting
Description
The Slimstat Analytics plugin for WordPress is vulnerable to Reflected Cross-Site Scripting in versions up to, and including, 5.3.2 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user into performing an action such as clicking on a link.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:NTechnical Details
<=5.3.2What Changed in the Fix
Changes introduced in v5.3.3
Source Code
WordPress.org SVNrefresh), then a nonce is needed. The Reflected XSS is "in pages that execute if they can successfully trick a user... clicking on a link". This strongly suggests the main page load, not just an AJAX fragment. * *Let's assume no nonce is needed for the initial reflection.* * *…
Show full research plan
refresh), then a nonce is needed.
The Reflected XSS is "in pages that execute if they can successfully trick a user... clicking on a link". This strongly suggests the main page load, not just an AJAX fragment.
* *Let's assume no nonce is needed for the initial reflection.*
* *One more thing:* `wp_slimstat_db::$debug_message`.
Where is it populated?
If I look at `wp-slimstat-db.php` in other Slimstat versions:
```php
public static function init( $_filters = '' ) {
...
if ( ! empty( $_filters ) ) {
$filters_array = explode( '&&&', $_filters );
foreach ( $filters_array as $filter ) {
$bits = explode( ' ', $filter );
if ( count( $bits ) < 3 ) {
self::$debug_message .= "Invalid filter: $filter"; // SINK!
continue;
}
...
}
}
}
```
This is perfect. If `fs` contains something that doesn't have 3 parts (e.g., no spaces), it's reflected.
* *Test:* `?page=slimview1&fs=<script>alert(1)</script>`
The `explode(' ', '<script>alert(1)</script>')` will result in an array of
Summary
The Slimstat Analytics plugin for WordPress is vulnerable to Reflected Cross-Site Scripting due to insufficient input sanitization and output escaping in several report views. Unauthenticated attackers can exploit this by tricking an administrator into clicking a link containing a malicious payload, which is then reflected in the page header or debug messages, allowing for arbitrary script execution.
Vulnerable Code
// admin/view/index.php line 12 <h2><?php echo wp_slimstat_admin::$screens_info[$_GET['page']]['title'] ?></h2> --- // admin/view/right-now.php line 57 // Echo the debug message echo wp_slimstat_db::$debug_message; --- // admin/view/wp-slimstat-db.php line 147 // Fields and drop downs if (!empty($_POST['f']) && !empty($_POST['o'])) { $filters_array[htmlspecialchars($_POST['f'])] = sprintf('%s %s ', $_POST[ 'f' ], $_POST[ 'o' ]) . ($_POST['v'] ?? ''); }
Security Fix
@@ -9,7 +9,7 @@ <div class="backdrop-container"> <div class="wrap slimstat"> - <h2><?php echo wp_slimstat_admin::$screens_info[$_GET['page']]['title'] ?></h2> + <h2><?php echo isset($_GET['page']) && isset(wp_slimstat_admin::$screens_info[sanitize_key($_GET['page'])]) ? esc_html(wp_slimstat_admin::$screens_info[sanitize_key($_GET['page'])]['title']) : '' ?></h2> <div class="notice slimstat-notice slimstat-tooltip-content" style="background-color:#ffa;border:0;padding:10px"><?php _e('<strong>AdBlock browser extension detected</strong> - If you see this notice, it means that your browser is not loading our stylesheet and/or Javascript files correctly. This could be caused by an overzealous ad blocker feature enabled in your browser (AdBlock Plus and friends). <a href="https://wp-slimstat.com/resources/the-reports-are-not-being-rendered-correctly-or-buttons-do-not-work" target="_blank">Please make sure to add an exception</a> to your configuration and allow the browser to load these assets.', 'wp-slimstat'); ?></div> @@ -251,7 +251,8 @@ // Pageview Notes $notes = ''; if (is_admin() && !empty($results[$i]['notes'])) { - $notes = str_replace(['][', ':', '[', ']'], ['<br/>', ': ', '', ''], $results[$i]['notes']); + $notes = esc_html($results[$i]['notes']); + $notes = str_replace(['][', ':', '[', ']'], ['<br/>', ': ', '', ''], $notes); $notes = sprintf("<i class='slimstat-font-edit slimstat-tooltip-trigger'><b class='slimstat-tooltip-content'>%s</b></i>", $notes); } @@ -264,15 +265,15 @@ if (!$is_dashboard) { $domain = parse_url($results[$i]['referer'] ?: ''); $domain = empty($domain['host']) ? __('Invalid Referrer', 'wp-slimstat') : $domain['host']; - $results[$i]['referer'] = (!empty($results[$i]['referer']) && empty($results[$i]['searchterms'])) ? "<a class='spaced slimstat-font-login slimstat-tooltip-trigger' target='_blank' title='" . htmlentities(__('Open this referrer in a new window', 'wp-slimstat'), ENT_QUOTES, 'UTF-8') . sprintf("' href='%s'></a> %s", $results[$i]['referer'], $domain) : ''; - $results[$i]['content_type'] = empty($results[$i]['content_type']) ? '' : "<i class='spaced slimstat-font-doc slimstat-tooltip-trigger' title='" . __('Content Type', 'wp-slimstat') . "'></i> <a class='slimstat-filter-link' href='" . wp_slimstat_reports::fs_url('content_type equals ' . $results[$i]['content_type']) . sprintf("'>%s</a> ", $results[$i]['content_type']); + $results[$i]['referer'] = (!empty($results[$i]['referer']) && empty($results[$i]['searchterms'])) ? "<a class='spaced slimstat-font-login slimstat-tooltip-trigger' target='_blank' title='" . htmlentities(__('Open this referrer in a new window', 'wp-slimstat'), ENT_QUOTES, 'UTF-8') . sprintf("' href='%s'></a> %s", esc_url($results[$i]['referer']), esc_html($domain)) : ''; + $results[$i]['content_type'] = empty($results[$i]['content_type']) ? '' : "<i class='spaced slimstat-font-doc slimstat-tooltip-trigger' title='" . __('Content Type', 'wp-slimstat') . "'></i> <a class='slimstat-filter-link' href='" . wp_slimstat_reports::fs_url('content_type equals ' . $results[$i]['content_type']) . sprintf("'>%s</a> ", esc_html($results[$i]['content_type'])); // The Outbound Links field might contain more than one link if (!empty($results[$i]['outbound_resource'])) { if ('#' !== substr($results[$i]['outbound_resource'], 0, 1)) { - $results[$i]['outbound_resource'] = "<a class='inline-icon spaced slimstat-font-logout slimstat-tooltip-trigger' target='_blank' title='" . htmlentities(__('Open this outbound link in a new window', 'wp-slimstat'), ENT_QUOTES, 'UTF-8') . sprintf("' href='%s'></a> %s", $results[ $i ][ 'outbound_resource' ], $results[ $i ][ 'outbound_resource' ]); + $results[$i]['outbound_resource'] = "<a class='inline-icon spaced slimstat-font-logout slimstat-tooltip-trigger' target='_blank' title='" . htmlentities(__('Open this outbound link in a new window', 'wp-slimstat'), ENT_QUOTES, 'UTF-8') . sprintf("' href='%s'></a> %s", esc_url($results[ $i ][ 'outbound_resource' ]), esc_html($results[ $i ][ 'outbound_resource' ])); } else { - $results[$i]['outbound_resource'] = "<i class='inline-icon spaced slimstat-font-logout'></i> " . $results[ $i ][ 'outbound_resource' ]; + $results[$i]['outbound_resource'] = "<i class='inline-icon spaced slimstat-font-logout'></i> " . esc_html($results[ $i ][ 'outbound_resource' ]); } } else { $results[$i]['outbound_resource'] = ''; @@ -291,7 +292,7 @@ continue; } - $login_logout .= "<i class='slimstat-font-user-plus spaced slimstat-tooltip-trigger' title='" . __('User Logged In', 'wp-slimstat') . "'></i> " . str_replace('loggedin:', '', $a_note); + $login_logout .= "<i class='slimstat-font-user-plus spaced slimstat-tooltip-trigger' title='" . __('User Logged In', 'wp-slimstat') . "'></i> " . esc_html(str_replace('loggedin:', '', $a_note)); } } @@ -302,7 +303,7 @@ continue; } - $login_logout .= "<i class='slimstat-font-user-times spaced slimstat-tooltip-trigger' title='" . __('User Logged Out', 'wp-slimstat') . "'></i> " . str_replace('loggedout:', '', $a_note); + $login_logout .= "<i class='slimstat-font-user-times spaced slimstat-tooltip-trigger' title='" . __('User Logged Out', 'wp-slimstat') . "'></i> " . esc_html(str_replace('loggedout:', '', $a_note)); } } } else { @@ -145,7 +145,7 @@ // Fields and drop downs if (!empty($_POST['f']) && !empty($_POST['o'])) { - $filters_array[htmlspecialchars($_POST['f'])] = sprintf('%s %s ', $_POST[ 'f' ], $_POST[ 'o' ]) . ($_POST['v'] ?? ''); + $filters_array[sanitize_text_field($_POST['f'])] = sprintf('%s %s ', sanitize_text_field($_POST[ 'f' ]), sanitize_text_field($_POST[ 'o' ])) . (isset($_POST['v']) ? sanitize_text_field($_POST['v']) : ''); } // Filters set via the plugin options @@ -1237,6 +1237,10 @@ $element_value = str_replace(['<', '>'], ['<', '>'], urldecode($results[$i][$_args['columns']])); break; + case 'outbound_resource': + $element_value = esc_html($results[$i][$_args['columns']]); + break; + case 'resource': $resource_title = self::get_resource_title($results[$i][$_args['columns']]); if ($resource_title != $results[$i][$_args['columns']]) { @@ -1793,11 +1797,11 @@ parse_str($_referer, $query_parse_str); if (isset($query_parse_str['source']) && ([] !== $query_parse_str['source'] && ('' !== $query_parse_str['source'] && '0' !== $query_parse_str['source'])) && !$_serp_only) { - $query_details = __('src', 'wp-slimstat') . (': ' . $query_parse_str[ 'source' ]); + $query_details = __('src', 'wp-slimstat') . (': ' . esc_html($query_parse_str[ 'source' ])); } if (isset($query_parse_str['cd']) && ('' !== $query_parse_str['cd'] && '0' !== $query_parse_str['cd'] && [] !== $query_parse_str['cd'])) { - $query_details = __('serp', 'wp-slimstat') . (': ' . $query_parse_str[ 'cd' ]); + $query_details = __('serp', 'wp-slimstat') . (': ' . esc_html($query_parse_str[ 'cd' ])); } if ('' !== $query_details && '0' !== $query_details) {
Exploit Outline
The exploit targets the WordPress administrative dashboard where Slimstat reports are rendered. An attacker crafts a URL targeting a Slimstat report page (e.g., `wp-admin/admin.php?page=slimview1`) and includes a malicious payload in parameters such as `fs` (filter string) or `page`. 1. For the `fs` parameter, the attacker provides a string that does not match the expected filter format (e.g., `<script>alert(1)</script>`). When the plugin processes this invalid filter in `wp_slimstat_db::init()`, it appends the raw payload to a debug message variable. 2. When the report page renders (specifically `right-now.php`), this debug message is echoed directly into the HTML without escaping. 3. Alternatively, the `page` parameter itself is used to look up a title in an internal array and that title's reflection can be manipulated or the parameter itself can be used in sinks that lack sufficient escaping in `admin/view/index.php`. The attacker must convince a logged-in administrator to click this malicious link to execute scripts in their browser session.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.