CVE-2025-13431

SlimStat Analytics <= 5.3.1 - Authenticated (Subscriber+) SQL Injection via `args` Parameter

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
5.3.2
Patched in
1d
Time to patch

Description

The SlimStat Analytics plugin for WordPress is vulnerable to time-based SQL Injection via the ‘args’ parameter in all versions up to, and including, 5.3.1 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with Subscriber-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=5.3.1
PublishedFebruary 10, 2026
Last updatedFebruary 11, 2026
Affected pluginwp-slimstat

What Changed in the Fix

Changes introduced in v5.3.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on **CVE-2025-13431**, a time-based SQL Injection vulnerability in the SlimStat Analytics plugin for WordPress. ### 1. Vulnerability Summary The **SlimStat Analytics** plugin (up to 5.3.1) is vulnerable to authenticated SQL injection via the `args` parameter in the `ajaxF…

Show full research plan

This research plan focuses on CVE-2025-13431, a time-based SQL Injection vulnerability in the SlimStat Analytics plugin for WordPress.

1. Vulnerability Summary

The SlimStat Analytics plugin (up to 5.3.1) is vulnerable to authenticated SQL injection via the args parameter in the ajaxFetchChartData method within src/Modules/Chart.php. The vulnerability arises because the plugin accepts a JSON-encoded object via $_POST['args'], decodes it, and uses its keys/values to construct SQL queries without sufficient sanitization or the use of $wpdb->prepare(). Specifically, the plugin uses these arguments to build complex analytic queries where string concatenation is preferred over parameterization for dynamic WHERE or GROUP BY clauses.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: slimstat_fetch_chart_data (Inferred from the method name ajaxFetchChartData in src/Modules/Chart.php)
  • Vulnerable Parameter: args (a JSON-encoded string)
  • Authentication: Authenticated, Subscriber level or higher.
  • Nonce Requirement: Yes, a nonce for the action slimstat_chart_nonce is required, as checked by check_ajax_referer('slimstat_chart_nonce', 'nonce') in src/Modules/Chart.php.

3. Code Flow

  1. Entry Point: A Subscriber user sends a POST request to admin-ajax.php with action=slimstat_fetch_chart_data.
  2. Nonce Verification: Chart::ajaxFetchChartData() calls check_ajax_referer('slimstat_chart_nonce', 'nonce').
  3. Input Parsing:
    • $args = isset($_POST['args']) ? json_decode(stripslashes($_POST['args']), true) : []; (Line 50 in src/Modules/Chart.php).
  4. Database Initialization:
    • The code ensures \wp_slimstat_db is initialized and sets range filters using $args['start'] and $args['end'].
  5. SQL Construction:
    • The Chart instance calls init($args), which leads to fetchChartData($normalized).
    • fetchChartData calls $this->buildSql($args, $prevArgs) (Line 158).
    • buildSql calls internal methods like sqlFor('HOUR', ...) which utilize values from the $args array to concatenate into the query string.
  6. Sink:
    • $results = $wpdb->get_results($sqlInfo['sql']); (Line 159). Since the query is built via concatenation in buildSql and the underlying sqlFor helpers, an attacker can inject SQL commands.

4. Nonce Acquisition Strategy

The nonce is localized for use in the WordPress admin dashboard where analytics charts are displayed. Even Subscriber-level users can typically access certain dashboard widgets if enabled.

  1. Preparation: Create a page containing a SlimStat shortcode to ensure all necessary scripts and nonces are enqueued.
    • wp post create --post_type=page --post_title="Analytics" --post_status=publish --post_content='[slimstat_chart]'
  2. Navigation: Navigate to the created page as a Subscriber user.
  3. Extraction: Use browser_eval to find the JavaScript object containing the nonce.
    • SlimStat typically localizes data into an object named SlimStatParams or SlimstatParams.
    • Action: browser_eval("window.SlimStatParams?.nonce || window.SlimstatParams?.nonce")
    • Note: If the shortcode doesn't work, check the Dashboard (/wp-admin/index.php) as SlimStat often enqueues its chart nonce for the "At a Glance" or "Slimstat Overview" widgets.

5. Exploitation Strategy

The goal is to trigger a SLEEP() command to confirm the SQL injection.

HTTP Request (Playwright http_request):

  • Method: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body Parameters:
    • action: slimstat_fetch_chart_data
    • nonce: [EXTRACTED_NONCE]
    • granularity: daily
    • args: A JSON-encoded payload. Based on buildSql, the injection point is likely within a filter key or an unexpected key that gets concatenated into the WHERE clause.
    • Payload Example:
      {
        "start": 1704067200,
        "end": 1735689600,
        "filter": "1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)"
      }
      
    • Alternative payload (if the injection is in the granularity check bypass or other keys):
      {
        "start": "1704067200' AND SLEEP(5) AND '1'='1",
        "end": 1735689600
      }
      

6. Test Data Setup

  1. User: Create a subscriber user: wp user create attacker attacker@example.com --role=subscriber --user_pass=password.
  2. Plugin Config: Ensure SlimStat is active: wp plugin activate wp-slimstat.
  3. Content: Create a page with the shortcode to leak the nonce:
    • wp post create --post_type=page --post_status=publish --post_title="Leak" --post_content='[slimstat_chart]'
  4. Baseline: Log in as attacker and navigate to the page to confirm the nonce exists.

7. Expected Results

  • Vulnerable Response: The HTTP request to admin-ajax.php will take approximately 5 seconds longer than a normal request.
  • Success Indicator: A JSON response containing {"success":true, "data": ...} or a database error if the sleep is successful but the rest of the query breaks.

8. Verification Steps

  1. Database Check: Use wp db query to check for the presence of the wp_slim_stats table to ensure the plugin is initialized.
  2. Timing: Compare the total_time of a baseline request (valid args) vs the exploit request (injected SLEEP).
  3. Error Logging: Check wp-content/debug.log (if WP_DEBUG is on) for SQL syntax errors which confirm the point of injection:
    • tail -f wp-content/debug.log | grep "WordPress database error"

9. Alternative Approaches

  • Granularity Injection: The granularity parameter in Chart.php:ajaxFetchChartData is sanitized with sanitize_text_field, but the $args['granularity'] used later might be manipulated.
  • Boolean-based: Instead of SLEEP(), use OR (1=1) or OR (1=2) and compare the totals object returned in the JSON response:
    • wp_send_json_success(['totals' => $totals, ...]) (Line 81).
    • Differences in the v1 or v2 values in the response between 1=1 and 1=2 confirm boolean-based injection.
  • Key Discovery: If filter is not the correct key, try injecting into start, end, or adding custom keys like where or having which the plugin might dynamically process into the SQL string.
Research Findings
Static analysis — not yet PoC-verified

Summary

SlimStat Analytics is vulnerable to authenticated SQL injection via the 'args' parameter in its AJAX chart data fetching functionality. The plugin decodes a JSON object and uses its values directly in SQL queries without sufficient sanitization or preparation, allowing users with Subscriber-level access or higher to perform time-based SQL injection.

Vulnerable Code

// src/Modules/Chart.php:48
public static function ajaxFetchChartData()
{
    check_ajax_referer('slimstat_chart_nonce', 'nonce');

    $args        = isset($_POST['args']) ? json_decode(stripslashes($_POST['args']), true) : [];
    $granularity = isset($_POST['granularity']) ? sanitize_text_field($_POST['granularity']) : 'daily';

    if (!in_array($granularity, ['yearly', 'monthly', 'weekly', 'daily', 'hourly'], true)) {
        wp_send_json_error(['message' => __('Invalid granularity', 'wp-slimstat')]);
    }

    if (!class_exists('\wp_slimstat_db')) {
        include_once SLIMSTAT_DIR . '/admin/view/wp-slimstat-db.php';
        \wp_slimstat_db::init();
    }

    \wp_slimstat_db::$filters_normalized['utime']['start'] = $args['start'];
    \wp_slimstat_db::$filters_normalized['utime']['end']   = $args['end'];

---

// src/Modules/Chart.php:156
private function fetchChartData(array $args): array
{
    global $wpdb;

    $prevArgs = $this->calculatePreviousArgs($args);
    $sqlInfo  = $this->buildSql($args, $prevArgs);
    $results  = $wpdb->get_results($sqlInfo['sql']);
    $totals   = $wpdb->get_results($sqlInfo['totalsSql']);

    return $this->processResults(
        $results,
        $totals,
        $sqlInfo['params'],
        $args['start'],
        $args['end'],
        $prevArgs['start'],
        $prevArgs['end']
    );
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.1/src/Modules/Chart.php /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.2/src/Modules/Chart.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.1/src/Modules/Chart.php	2025-08-25 08:38:44.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-slimstat/5.3.2/src/Modules/Chart.php	2025-11-24 05:41:38.000000000 +0000
@@ -41,12 +41,26 @@
     {
         check_ajax_referer('slimstat_chart_nonce', 'nonce');
 
+        // Additional capability check - users must be able to view stats
+        $minimum_capability = 'read';
+        if (!current_user_can($minimum_capability)) {
+            wp_send_json_error(['message' => __('Insufficient permissions', 'wp-slimstat')]);
+        }
+
         $args        = isset($_POST['args']) ? json_decode(stripslashes($_POST['args']), true) : [];
         $granularity = isset($_POST['granularity']) ? sanitize_text_field($_POST['granularity']) : 'daily';
 
         if (!in_array($granularity, ['yearly', 'monthly', 'weekly', 'daily', 'hourly'], true)) {
             wp_send_json_error(['message' => __('Invalid granularity', 'wp-slimstat')]);
         }
+
+        // Validate and sanitize start/end timestamps
+        if (isset($args['start'])) {
+            $args['start'] = absint($args['start']);
+        }
+        if (isset($args['end'])) {
+            $args['end'] = absint($args['end']);
+        }
 
         if (!class_exists('\wp_slimstat_db')) {
             include_once SLIMSTAT_DIR . '/admin/view/wp-slimstat-db.php';
@@ -216,8 +230,16 @@
         global $wpdb;
         $data1 = $args['chart_data']['data1'] ?? '';
         $data2 = $args['chart_data']['data2'] ?? '';
-
-        $start = $args['start'];
-        $end   = $args['end'];
+
+        // Validate SQL expressions to prevent SQL injection
+        $data1 = $this->validateSqlExpression($data1);
+        $data2 = $this->validateSqlExpression($data2);
+
+        // Ensure timestamps are integers (defense in depth)
+        $start = absint($args['start']);
+        $end   = absint($args['end']);
+        $prevStart = absint($prevArgs['start']);
+        $prevEnd = absint($prevArgs['end']);
 
         $totalOffsetSeconds = (int) $wpdb->get_var('SELECT TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), NOW())');
         $sign               = ($totalOffsetSeconds < 0) ? '+' : '-';

Exploit Outline

The vulnerability is exploited via an AJAX request to the 'slimstat_fetch_chart_data' action. 1. An attacker authenticates as a Subscriber and retrieves a valid 'slimstat_chart_nonce' from the WordPress dashboard or a page using SlimStat shortcodes. 2. The attacker crafts a POST request to '/wp-admin/admin-ajax.php' containing the 'args' parameter as a JSON-encoded string. 3. Within the 'args' JSON object, the attacker injects SQL payloads into keys such as 'start' or 'end'. For example: { "start": "1735689600 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)", "end": 1735689700 }. 4. Because the plugin uses these values directly in string concatenation to build the SQL query (specifically in the 'sqlFor' helper methods used by 'buildSql'), the SLEEP() command is executed by the database. 5. The attacker observes the delay in response to confirm the injection.

Check if your site is affected.

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