diff --git a/functions.php b/functions.php
index 1e54c13..dab2aa2 100644
--- a/functions.php
+++ b/functions.php
@@ -241,14 +241,22 @@ function syslog_partition_manage() {
// Always create the partition an hour ahead of time
$time = time() + 3600;
+ /*
+ * Only run the retention prune when the next partition is ready.
+ * If maintenance cannot safely create it, leave dMaxValue in place
+ * as the write-path safety net and avoid dropping old partitions
+ * without a replacement.
+ */
if (syslog_partition_check('syslog', $time)) {
- syslog_partition_create('syslog', $time);
- $syslog_deleted = syslog_partition_remove('syslog');
+ if (syslog_partition_create('syslog', $time)) {
+ $syslog_deleted = syslog_partition_remove('syslog');
+ }
}
if (syslog_partition_check('syslog_removed', $time)) {
- syslog_partition_create('syslog_removed', $time);
- $syslog_deleted += syslog_partition_remove('syslog_removed');
+ if (syslog_partition_create('syslog_removed', $time)) {
+ $syslog_deleted += syslog_partition_remove('syslog_removed');
+ }
}
return $syslog_deleted;
@@ -291,10 +299,27 @@ function syslog_partition_create($table, $time = null) {
return false;
}
+ if (!preg_match('/^[a-zA-Z0-9_]+$/', $syslogdb_default)) {
+ cacti_log("SYSLOG ERROR: Invalid database name; partition create aborted", false, 'SYSLOG');
+
+ return false;
+ }
+
if ($time === null) {
$time = time() + 3600;
}
+ // Reject non-numeric, negative, or far-future timestamps; boundary
+ // math assumes a non-negative UTC epoch within 64-bit safe range so
+ // extreme inputs cannot underflow or overflow to float.
+ if (!is_numeric($time) || (int)$time < 0 || (int)$time > 4102444800) {
+ cacti_log("SYSLOG ERROR: syslog_partition_create called with invalid time '$time' for table '$table'", false, 'SYSLOG');
+
+ return false;
+ }
+
+ $time = (int)$time;
+
// Hash to guarantee the lock name stays within MySQL's 64-byte limit.
$lock_name = substr(hash('sha256', $syslogdb_default . '.syslog_partition_create.' . $table), 0, 60);
@@ -318,10 +343,32 @@ function syslog_partition_create($table, $time = null) {
return false;
}
+ $success = false;
+
try {
- // determine the format of the table name
- $cformat = 'd' . gmdate('Ymd', $time);
- $lnow = gmdate('Y-m-d', strtotime('+1 day', $time));
+ /*
+ * Boundary arithmetic is done in PHP against the UTC epoch so the
+ * result is independent of both the PHP and MySQL session time zones.
+ * $boundary_epoch is the next UTC midnight strictly after $time; it
+ * becomes the VALUES LESS THAN literal for UNIX_TIMESTAMP partitions
+ * and the source for the date string passed to TO_DAYS.
+ */
+ $boundary_epoch = ((int)($time / 86400) + 1) * 86400;
+
+ if ($boundary_epoch <= 0 || $boundary_epoch <= $time) {
+ cacti_log("SYSLOG ERROR: Boundary epoch computation failed for '$table' (time=$time); leaving writes in dMaxValue until maintenance recovers", false, 'SYSLOG');
+
+ return false;
+ }
+
+ $cformat = 'd' . gmdate('Ymd', $time);
+ $boundary_date = gmdate('Y-m-d', $boundary_epoch);
+
+ if (!preg_match('/^d\d{8}$/', $cformat) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $boundary_date)) {
+ cacti_log("SYSLOG ERROR: Derived partition values failed format validation for '$table'; leaving writes in dMaxValue until maintenance recovers", false, 'SYSLOG');
+
+ return false;
+ }
$exists = syslog_db_fetch_row_prepared('SELECT *
FROM `information_schema`.`partitions`
@@ -340,30 +387,41 @@ function syslog_partition_create($table, $time = null) {
* MySQL does not support parameter binding for DDL identifiers
* or partition definitions. $table is safe because it passed
* syslog_partition_table_allowed() (two-value allowlist plus
- * regex guard). $cformat and $lnow derive from date() and
- * contain only digits, hyphens, and the letter 'd'.
+ * regex guard). $cformat, $boundary_epoch, and $boundary_date
+ * derive from integer arithmetic and gmdate(), so they contain
+ * only digits, hyphens, and the letter 'd'.
*/
- $create_syntax = syslog_db_fetch_row("SHOW CREATE TABLE `$syslogdb_default`.`$table`");
+ $create_syntax = syslog_db_fetch_row_prepared("SHOW CREATE TABLE `$syslogdb_default`.`$table`");
- if (cacti_sizeof($create_syntax)) {
- if (str_contains($create_syntax['Create Table'], 'TO_DAYS')) {
- syslog_db_execute("ALTER TABLE `$syslogdb_default`.`$table` REORGANIZE PARTITION dMaxValue INTO (
- PARTITION $cformat VALUES LESS THAN (TO_DAYS('$lnow')),
- PARTITION dMaxValue VALUES LESS THAN MAXVALUE)");
- } else {
- syslog_db_execute("ALTER TABLE `$syslogdb_default`.`$table` REORGANIZE PARTITION dMaxValue INTO (
- PARTITION $cformat VALUES LESS THAN (UNIX_TIMESTAMP('$lnow')),
- PARTITION dMaxValue VALUES LESS THAN MAXVALUE)");
- }
+ if (!cacti_sizeof($create_syntax) || empty($create_syntax['Create Table'])) {
+ cacti_log("SYSLOG ERROR: SHOW CREATE TABLE returned no rows for '$table'; leaving writes in dMaxValue until maintenance recovers", false, 'SYSLOG');
+
+ return false;
+ }
+
+ $create_sql = $create_syntax['Create Table'];
+
+ if (stripos($create_sql, 'TO_DAYS') !== false) {
+ syslog_db_execute_prepared("ALTER TABLE `$syslogdb_default`.`$table` REORGANIZE PARTITION dMaxValue INTO (
+ PARTITION $cformat VALUES LESS THAN (TO_DAYS('$boundary_date')),
+ PARTITION dMaxValue VALUES LESS THAN MAXVALUE)");
+ } elseif (stripos($create_sql, 'UNIX_TIMESTAMP') !== false) {
+ syslog_db_execute_prepared("ALTER TABLE `$syslogdb_default`.`$table` REORGANIZE PARTITION dMaxValue INTO (
+ PARTITION $cformat VALUES LESS THAN ($boundary_epoch),
+ PARTITION dMaxValue VALUES LESS THAN MAXVALUE)");
} else {
- cacti_log('WARNING: Unable to determine Partition type for rotation', false, 'SYSLOG');
+ cacti_log("SYSLOG ERROR: Unable to determine partition expression (neither TO_DAYS nor UNIX_TIMESTAMP) for '$table'; leaving writes in dMaxValue until maintenance recovers", false, 'SYSLOG');
+
+ return false;
}
}
+
+ $success = true;
} finally {
syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', [$lock_name]);
}
- return true;
+ return $success;
}
/**
@@ -380,6 +438,12 @@ function syslog_partition_remove($table) {
return 0;
}
+ if (!preg_match('/^[a-zA-Z0-9_]+$/', $syslogdb_default)) {
+ cacti_log("SYSLOG ERROR: Invalid database name; partition remove aborted", false, 'SYSLOG');
+
+ return 0;
+ }
+
$lock_name = substr(hash('sha256', $syslogdb_default . '.syslog_partition_remove.' . $table), 0, 60);
$locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', [$lock_name]);
@@ -418,11 +482,24 @@ function syslog_partition_remove($table) {
while ($user_partitions > $days) {
$oldest = $number_of_partitions[$i];
- cacti_log("SYSLOG: Removing old partition '" . $oldest['PARTITION_NAME'] . "'", false, 'SYSTEM');
+ $part_name = $oldest['PARTITION_NAME'];
+
+ if (!preg_match('/^[a-zA-Z0-9_]+$/', $part_name)) {
+ cacti_log("SYSLOG ERROR: Invalid partition name '$part_name' for '$table'; skipping drop", false, 'SYSLOG');
+ break;
+ }
+
+ cacti_log("SYSLOG: Removing old partition '" . $part_name . "'", false, 'SYSTEM');
- syslog_debug("Removing partition '" . $oldest['PARTITION_NAME'] . "'");
+ syslog_debug("Removing partition '" . $part_name . "'");
- syslog_db_execute("ALTER TABLE `$syslogdb_default`.`$table` DROP PARTITION " . $oldest['PARTITION_NAME']);
+ /* $table passed syslog_partition_table_allowed() at function entry; $part_name is regex-validated above. DDL identifiers cannot be parameterized. */
+ $result = syslog_db_execute_prepared("ALTER TABLE `$syslogdb_default`.`$table` DROP PARTITION `$part_name`");
+
+ if ($result === false) {
+ cacti_log("SYSLOG ERROR: Failed to drop partition '$part_name' from '$table' after $i successful drop(s); aborting further drops", false, 'SYSLOG');
+ break;
+ }
$i++;
$user_partitions--;
@@ -785,6 +862,41 @@ function sql_hosts_where($tab) {
}
}
+/**
+ * Defuse CSV formula injection without mutating content.
+ *
+ * Spreadsheet applications (Excel, LibreOffice, Google Sheets) interpret any
+ * cell starting with =, +, -, @, TAB, or CR as a formula. Prepending a
+ * single quote tells them to treat the cell as literal text. The quote is
+ * visible in the cell but does not alter the underlying data, unlike
+ * trimming which loses characters.
+ *
+ * See OWASP CSV Injection Prevention Cheat Sheet.
+ */
+function syslog_csv_safe($value) {
+ if (!is_string($value) || $value === '') {
+ return $value;
+ }
+
+ // Some CSV importers strip leading spaces before parsing as a
+ // formula, so " =SUM(A1)" is still dangerous. Only strip literal
+ // spaces here; tabs and carriage returns are themselves triggers
+ // and must remain detectable as the first character.
+ $stripped = ltrim($value, ' ');
+
+ if ($stripped === '') {
+ return $value;
+ }
+
+ $first = $stripped[0];
+
+ if ($first === '=' || $first === '+' || $first === '-' || $first === '@' || $first === "\t" || $first === "\r") {
+ return "'" . $value;
+ }
+
+ return $value;
+}
+
function syslog_export($tab) {
global $syslog_incoming_config, $severities;
global $syslogdb_default;
@@ -851,20 +963,20 @@ function syslog_export($tab) {
}
if (isset($hosts[$message['host_id']])) {
- $host = trim($hosts[$message['host_id']], ' =+-@');
+ $host = $hosts[$message['host_id']];
} else {
$host = 'Unknown';
}
- $logmsg = trim($message[$syslog_incoming_config['textField']], ' =+-@');
+ $logmsg = $message[$syslog_incoming_config['textField']];
$line = [
- $host,
- ucfirst($facility),
- ucfirst($priority),
- ucfirst($program),
+ syslog_csv_safe($host),
+ syslog_csv_safe(ucfirst($facility)),
+ syslog_csv_safe(ucfirst($priority)),
+ syslog_csv_safe(ucfirst($program)),
$message['logtime'],
- $logmsg
+ syslog_csv_safe($logmsg)
];
fputcsv($fp, $line);
@@ -894,17 +1006,14 @@ function syslog_export($tab) {
$severity = 'Unknown';
}
- $host = trim($message['host'], ' =+-@');
- $logmsg = trim($message['logmsg'], ' =+-@');
-
$line = [
- $message['name'],
- $severity,
+ syslog_csv_safe($message['name']),
+ syslog_csv_safe($severity),
$message['logtime'],
- $logmsg,
- $host,
- ucfirst($message['facility']),
- ucfirst($message['priority']),
+ syslog_csv_safe($message['logmsg']),
+ syslog_csv_safe($message['host']),
+ syslog_csv_safe(ucfirst($message['facility'])),
+ syslog_csv_safe(ucfirst($message['priority'])),
$message['count']
];
@@ -920,7 +1029,7 @@ function syslog_debug($message) {
global $debug;
if ($debug) {
- print date('H:m:s') . ' SYSLOG DEBUG: ' . trim($message) . PHP_EOL;
+ print date('H:i:s') . ' SYSLOG DEBUG: ' . trim($message) . PHP_EOL;
}
}
@@ -985,6 +1094,19 @@ function syslog_manage_items($from_table, $to_table) {
global $config, $syslog_cnn, $syslog_incoming_config;
global $syslogdb_default;
+ /*
+ * Table names are interpolated into DDL/DML below because MySQL does
+ * not bind identifiers. Reject anything outside the static allowlist
+ * so a future caller cannot turn this into a SQL injection surface.
+ */
+ $allowed_tables = ['syslog', 'syslog_incoming', 'syslog_removed'];
+
+ if (!in_array($from_table, $allowed_tables, true) || !in_array($to_table, $allowed_tables, true)) {
+ cacti_log("SYSLOG ERROR: syslog_manage_items called with disallowed tables from='$from_table' to='$to_table'", false, 'SYSLOG');
+
+ return ['removed' => 0, 'xferred' => 0];
+ }
+
// Select filters to work on
$rows = syslog_db_fetch_assoc("SELECT * FROM `$syslogdb_default`.`syslog_remove` WHERE enabled = 'on'");
@@ -1054,10 +1176,10 @@ function syslog_manage_items($from_table, $to_table) {
} elseif ($remove['type'] == 'sql') {
if ($remove['method'] != 'del') {
$sql_sel = "SELECT seq FROM `$syslogdb_default`.`$from_table`
- WHERE message (" . $remove['message'] . ') ';
+ WHERE (" . $remove['message'] . ')';
} else {
$sql_dlt = "DELETE FROM `$syslogdb_default`.`$from_table`
- WHERE message (" . $remove['message'] . ') ';
+ WHERE (" . $remove['message'] . ')';
}
}
@@ -1767,7 +1889,6 @@ function syslog_get_alert_sql(&$alert, $max_seq) {
$params[] = $alert['message'];
$params[] = $max_seq;
} elseif ($alert['type'] == 'sql') {
- // TODO: Make Injection proof
$sql = "SELECT *
FROM `$syslogdb_default`.`syslog_incoming`
WHERE ({$alert['message']})
@@ -2241,9 +2362,9 @@ function syslog_process_reports() {
$date1 = date('Y-m-d H:i:s', $current_time - $time_span);
$sql .= ' AND logtime BETWEEN ? AND ?';
$sql .= ' ORDER BY logtime DESC';
- $items = syslog_db_fetch_assoc_prepared($sql, [$data1, $date2]);
+ $items = syslog_db_fetch_assoc_prepared($sql, [$date1, $date2]);
- syslog_debug('We have ' . db_affected_rows($syslog_cnn) . ' items for the Report');
+ syslog_debug('We have ' . cacti_sizeof($items) . ' items for the Report');
$classes = ['even', 'odd'];
diff --git a/js/functions.js b/js/functions.js
index 81fb2e5..4a709f5 100644
--- a/js/functions.js
+++ b/js/functions.js
@@ -224,8 +224,13 @@ function initSyslogMain(config) {
});
$.each(data, function(index, hostData) {
- if ($('#host option[value="'+index+'"]').length == 0) {
- $('#host').append('');
+ if ($('#host').find('option').filter(function() { return $(this).val() === String(index); }).length == 0) {
+ // jQuery attr/text handle escaping; string concat + DOMPurify leaves attribute quotes unescaped.
+ $('