--- a/wp-slimstat/src/Modules/Chart.php
+++ b/wp-slimstat/src/Modules/Chart.php
@@ -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';
@@ -79,7 +93,7 @@
'chart_labels' => $chart->chartLabels,
'translations' => $chart->translations,
]);
- } catch (Exception $exception) {
+ } catch (Exception $exception) {
wp_send_json_error(['message' => $exception->getMessage()]);
}
}
@@ -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) ? '+' : '-';
@@ -256,6 +278,11 @@
'YEAR' => ['label' => 'Y'],
];
+ // Note: $data1 and $data2 are already validated and safe
+ // All timestamps are sanitized as integers
+ // Table prefix comes from WordPress (safe)
+ // $tzOffset is calculated from DB query and formatted (safe)
+
$sql = "
SELECT
grouped_date AS dt,
@@ -273,7 +300,7 @@
END AS period,
{$dtExpr} AS grouped_date
FROM {$wpdb->prefix}slim_stats
- WHERE dt BETWEEN {$prevArgs['start']} AND {$prevArgs['end']}
+ WHERE dt BETWEEN {$prevStart} AND {$prevEnd}
OR dt BETWEEN {$start} AND {$end}
GROUP BY grouped_date, period
) AS grouped_data
@@ -290,7 +317,7 @@
ELSE 'previous'
END AS period
FROM {$wpdb->prefix}slim_stats
- WHERE CONVERT_TZ(FROM_UNIXTIME(dt), '+00:00', '{$tzOffset}') BETWEEN FROM_UNIXTIME({$prevArgs['start']}) AND FROM_UNIXTIME({$prevArgs['end']})
+ WHERE CONVERT_TZ(FROM_UNIXTIME(dt), '+00:00', '{$tzOffset}') BETWEEN FROM_UNIXTIME({$prevStart}) AND FROM_UNIXTIME({$prevEnd})
OR CONVERT_TZ(FROM_UNIXTIME(dt), '+00:00', '{$tzOffset}') BETWEEN FROM_UNIXTIME({$start}) AND FROM_UNIXTIME({$end})
GROUP BY period
ORDER BY period
@@ -303,6 +330,88 @@
];
}
+ /**
+ * Validates SQL expressions to prevent SQL injection attacks.
+ * Uses a predefined metrics system for maximum security.
+ *
+ * @param string $expression The SQL expression to validate
+ * @return string The safe SQL expression
+ * @throws Exception If the expression is invalid or potentially malicious
+ */
+ private function validateSqlExpression(string $expression): string
+ {
+ global $wpdb;
+
+ // Remove extra whitespace and normalize
+ $expression = preg_replace('/s+/', ' ', trim($expression));
+
+ // Empty expressions default to COUNT(*)
+ if (empty($expression)) {
+ return 'COUNT(*)';
+ }
+
+ // Define allowed columns from wp_slim_stats table
+ $allowedColumns = [
+ 'id', 'ip', 'other_ip', 'username', 'email',
+ 'country', 'location', 'city',
+ 'referer', 'resource', 'searchterms', 'notes', 'visit_id',
+ 'server_latency', 'page_performance',
+ 'browser', 'browser_version', 'browser_type', 'platform',
+ 'language', 'fingerprint', 'user_agent',
+ 'resolution', 'screen_width', 'screen_height',
+ 'content_type', 'category', 'author', 'content_id',
+ 'outbound_resource',
+ 'tz_offset', 'dt_out', 'dt'
+ ];
+
+ // Define allowed aggregate functions
+ $allowedFunctions = ['COUNT', 'SUM', 'AVG', 'MAX', 'MIN'];
+
+ // Strict pattern matching with anchors to prevent bypass attempts
+ // Pattern 1: COUNT(*) or SUM(*) etc (no spaces allowed in function name)
+ if (preg_match('/^(COUNT|SUM|AVG|MAX|MIN)s*(s**s*)$/i', $expression, $matches)) {
+ $function = strtoupper($matches[1]);
+ return $function . '(*)';
+ }
+
+ // Pattern 2: COUNT(column) or COUNT( column )
+ if (preg_match('/^(COUNT|SUM|AVG|MAX|MIN)s*(s*([a-z_][a-z0-9_]*)s*)$/i', $expression, $matches)) {
+ $function = strtoupper($matches[1]);
+ $column = strtolower($matches[2]);
+
+ if (!in_array($function, $allowedFunctions, true)) {
+ throw new Exception(__('Invalid SQL function in chart data expression', 'wp-slimstat'));
+ }
+
+ if (!in_array($column, $allowedColumns, true)) {
+ throw new Exception(__('Invalid column name in chart data expression', 'wp-slimstat'));
+ }
+
+ // Use esc_sql as additional protection (though column is whitelisted)
+ return $function . '( ' . esc_sql($column) . ' )';
+ }
+
+ // Pattern 3: COUNT(DISTINCT column) or COUNT( DISTINCT column )
+ if (preg_match('/^(COUNT|SUM|AVG|MAX|MIN)s*(s*DISTINCTs+([a-z_][a-z0-9_]*)s*)$/i', $expression, $matches)) {
+ $function = strtoupper($matches[1]);
+ $column = strtolower($matches[2]);
+
+ if (!in_array($function, $allowedFunctions, true)) {
+ throw new Exception(__('Invalid SQL function in chart data expression', 'wp-slimstat'));
+ }
+
+ if (!in_array($column, $allowedColumns, true)) {
+ throw new Exception(__('Invalid column name in chart data expression', 'wp-slimstat'));
+ }
+
+ // Use esc_sql as additional protection (though column is whitelisted)
+ return $function . '( DISTINCT ' . esc_sql($column) . ' )';
+ }
+
+ // If none of the patterns match, reject the expression
+ throw new Exception(__('Invalid SQL expression in chart data. Only whitelisted aggregate functions on valid columns are allowed.', 'wp-slimstat'));
+ }
+
private function processResults(array $rows, array $totals, array $params, int $start, int $end, int $prevStart, int $prevEnd): array
{
$buckets = new DataBuckets($params['label'], $params['gran'], $start, $end, $prevStart, $prevEnd, $totals);
--- a/wp-slimstat/wp-slimstat.php
+++ b/wp-slimstat/wp-slimstat.php
@@ -3,7 +3,7 @@
* Plugin Name: SlimStat Analytics
* Plugin URI: https://wp-slimstat.com/
* Description: The leading web analytics plugin for WordPress
- * Version: 5.3.1
+ * Version: 5.3.2
* Author: Jason Crouse, VeronaLabs
* Text Domain: wp-slimstat
* Domain Path: /languages
@@ -24,7 +24,7 @@
}
// Set the plugin version and directory
-define('SLIMSTAT_ANALYTICS_VERSION', '5.3.1');
+define('SLIMSTAT_ANALYTICS_VERSION', '5.3.2');
define('SLIMSTAT_FILE', __FILE__);
define('SLIMSTAT_DIR', __DIR__);
define('SLIMSTAT_URL', plugins_url('', __FILE__));