security: batch 7 develop - consolidate remaining develop-only GHSA advisories (12 fixes + 8 confirmations) (#8149)

* security: batch 7 develop - consolidate remaining develop-only GHSA advisories (12 fixes + 8 regression confirmations)

Fixes: f7jw (automation preview XSS), rp5g (db_column_exists LIKE), vx2m (automation pivot CASE WHEN), h5wg (plugin DDL identifiers), 929h (aggregate LIKE filter), mfw8 (add_graphs REGEXP), m49v (user_admin sort_column allowlist), mpfm (rrd error-image theme allowlist), m3fh (SNMPv3 control chars), cg45 (remember-me row revoke), xq26 (remember-me retention window), m67r (import xml_path containment).

Confirmed already fixed on develop, pinned with regression tests: 4gph, 977w, mgcq, 5p79, cfhh, wfcf, pjq5, wp33.

Excluded: 227p (1.2.x-only), mx5c (maintainer private branch).

* security: address batch-7 review feedback (lexical-first xml_path containment, session-wide sort allowlist, connection-aware db_column_exists quoting, reject-not-normalize plugin DDL identifiers, behavior-based test names)

* style: apply php-cs-fixer single-quote fix to api_automation pivot builders; add WSL dev-environment note to copilot instructions

* fix phpstan string typing

* test: refresh SQL interpolation baseline for db_column_exists db_qstr change (database.php 9->8)

---------

Co-authored-by: ci <ci@local>
This commit is contained in:
TheWitnessandci authored and GitHub committed 2026-10-02 15:27:34 -04:00
1 parent 5e870a91df
commit 9facd9b500
19 files changed
+310 -25

No files matched your search

+1
View File
@@ -70,6 +70,7 @@
- Escape output for HTML attribute contexts with `html_escape_attr()`, not `htmle()`/`html_escape()`; only the attribute helper double-encodes to stop pre-encoded entities from breaking out of the attribute.
## Workflows you’ll actually use
- **Windows/WSL**: When working in VS Code from a Windows machine, run all fixes, builds, linters, tests, and git operations inside WSL (a Linux distro) rather than native Windows. The toolchain (PHP 8.3, Composer, `php-cs-fixer`, Pest) and the repo's tab indentation / `\n` line-ending conventions are Linux-first; running them on native Windows produces spurious diffs and failures. Edit the WSL-mounted checkout (e.g. under `/mnt/c/...` or a native WSL path) and invoke `composer` scripts such as `composer php-cs-fixit` from the WSL shell.
- Install deps: `composer install` (CI validates via `.github/workflows/syntax.yml`).
- Install/upgrade DB: `php -q cli/install_cacti.php --accept-eula --install --force` and `php -q cli/upgrade_database.php --forcever=$(cat include/cacti_version)`.
- Run poller: `php poller.php --poller=1 --force --debug` (daemon debug: `./cactid.php --foreground --debug`).
+20
View File
@@ -5,6 +5,26 @@ Cacti CHANGELOG
-security#GHSA-m6wx-f538-m6q3: Apply the .log extension whitelist to path_boost_log to block log-poisoning RCE
-security#GHSA-73qf-hq23-c4m7: Authorize tree lock, unlock, and get_node against per-tree access before dispatch
-security#GHSA-967c-6qj7-q7rh: Confirm the rrdtool control-character strip closes the RRD-create |query_*| injection path
-security#GHSA-f7jw-gfhr-cxwm: Escape the automation tree-rule preview Resulting Branch values
-security#GHSA-rp5g-r5vp-q7j6: Quote the db_column_exists() LIKE term so the SQLi-defense helper is not itself injectable
-security#GHSA-vx2m-q53g-hmrf: Escape host_snmp_cache.field_name in the automation pivot CASE WHEN builders
-security#GHSA-h5wg-qf6f-7f6r: Confine plugin DDL table identifiers in api_plugin_drop_table and api_plugin_db_table_create
-security#GHSA-929h-4c93-hxcg: Quote the aggregate_make_sql_where() LIKE filter term
-security#GHSA-mfw8-r3jx-67ff: Use db_qstr() instead of addslashes() for the cli add_graphs REGEXP clause
-security#GHSA-m49v-hr7h-wwcj: Restrict the user_admin sort_column to displayed columns so ORDER BY cannot pivot onto user_auth secrets
-security#GHSA-mpfm-qhvj-5879: Validate the theme in rrdtool_create_error_image() through the theme allowlist
-security#GHSA-m3fh-gxqj-76hq: Strip control characters from the SNMPv3 credential device fields
-security#GHSA-cg45-2mww-g98p: Revoke the server-side remember-me row on the auth_changepassword nopassword path
-security#GHSA-xq26-f282-f998: Enforce a retention window on the remember-me cookie token check
-security#GHSA-m67r-fcmw-gvv7: Confine the imported data-query xml_path to the Cacti tree
-security#GHSA-4gph-8x7q-h365: Confirm the report-add device description status message is escaped (regression test)
-security#GHSA-977w-79m7-xjc4: Confirm the graph export legend columns are html-escaped (regression test)
-security#GHSA-mgcq-r6vm-4xhw: Confirm the Reports Data Query name is html-escaped (regression test)
-security#GHSA-5p79-q5q3-84vw: Confirm die_html_input_error escapes the echoed variable and value (regression test)
-security#GHSA-cfhh-pwvx-gp5g: Confirm the aggregate rfilter is no longer echoed into a raw HTML attribute (regression test)
-security#GHSA-wfcf-2x67-2jxg: Confirm the user_admin group filter is a bound parameter (regression test)
-security#GHSA-pjq5-3268-g222: Confirm path_dsstats_log is escaped before the dsstats shell redirect (regression test)
-security#GHSA-wp33-g6gc-444m: Confirm the data_templates rrd_maximum/rrd_minimum validator is anchored (regression test)
-security#GHSA-69vw-qxxv-p8w2: Stop auto-granting an admin session when auth_method is None and fail closed pending a manual reset
-security#GHSA-r7hc-hrpv-52gf: Validate host_id before it is concatenated into the graph_view list and preview filter WHERE clauses (#8100) - Special Thanks to Jiecub3, Aur0ra-m and lz2y
-security#GHSA-6mjm-5vwr-3fr2: Validate and bind local_graph_id in the aggregate graph item sequence query (#8099) - Special Thanks to Jiecub3, Aur0ra-m and lz2y
+1 -1
View File
@@ -1839,7 +1839,7 @@ function aggregate_make_sql_where(string $sql_where, array $items, string $field
} elseif (cacti_strtolower($i) == 'or') {
$sql_where .= ' OR ';
} else {
$sql_where .= ($termcount > 0 ? ' OR ' : '') . $field . " LIKE '%" . trim($i) . "%'";
$sql_where .= ($termcount > 0 ? ' OR ' : '') . $field . ' LIKE ' . db_qstr('%' . trim($i) . '%');
$termcount++;
}
}
+2
View File
@@ -98,6 +98,8 @@ if ($user['password_change'] != 'on') {
// destroy session information
kill_session_var(SESS_USER_ID);
// GHSA-cg45-2mww-g98p: revoke the server-side remember-me row too, not just the browser cookie.
clear_auth_cookie();
cacti_cookie_logout();
$return = validate_redirect_url($_SERVER['HTTP_REFERER'] ?? '', 'index.php');
+2 -2
View File
@@ -741,7 +741,7 @@ if (cacti_sizeof($parms)) {
if (isset($dsGraph['snmpValue'][$index_snmp_filter])) {
$req .= ' AND field_value = ' . db_qstr($dsGraph['snmpValue'][$index_snmp_filter]) . ')';
} elseif (isset($dsGraph['snmpValueRegex'][$index_snmp_filter])) {
$req .= ' AND field_value REGEXP "' . addslashes($dsGraph['snmpValueRegex'][$index_snmp_filter]) . '")';
$req .= ' AND field_value REGEXP ' . db_qstr($dsGraph['snmpValueRegex'][$index_snmp_filter]) . ')';
}
$index_snmp_filter++;
@@ -754,7 +754,7 @@ if (cacti_sizeof($parms)) {
foreach ($dsGraph['snmpFieldExclude'] as $snmpField) {
$req .= ' AND snmp_index NOT IN (
SELECT DISTINCT snmp_index FROM host_snmp_cache WHERE host_id=' . $host_id . ' AND field_name = ' . db_qstr($snmpField);
$req .= ' AND field_value REGEXP "' . addslashes($dsGraph['snmpValueExclude'][$index_snmp_filter]) . '")';
$req .= ' AND field_value REGEXP ' . db_qstr($dsGraph['snmpValueExclude'][$index_snmp_filter]) . ')';
$index_snmp_filter++;
}
}
+6 -5
View File
@@ -1271,9 +1271,10 @@ function display_matching_trees(int $rule_id, int $rule_type, array $item, strin
for ($j = 0; cacti_sizeof($replacement); $j++) {
if ($j > 0) {
$repl .= '<br>';
$repl .= str_pad('', $j * 3, '-') . '&nbsp;' . array_shift($replacement);
// GHSA-f7jw-gfhr-cxwm: the replacement values carry attacker-influenced device/data fields; escape them while keeping the <br> layout.
$repl .= str_pad('', $j * 3, '-') . '&nbsp;' . htmle((string) array_shift($replacement));
} else {
$repl = array_shift($replacement);
$repl = htmle((string) array_shift($replacement));
}
}
@@ -1883,7 +1884,7 @@ function build_data_query_sql(array $rule) : string {
if (cacti_sizeof($field_names) > 0) {
foreach (($field_names ?: []) as $column) {
$field_name = $column['field_name'];
$sql_query .= ",\n\tMAX(CASE WHEN field_name='$field_name' THEN field_value ELSE NULL END) AS '$field_name'";
$sql_query .= ",\n\tMAX(CASE WHEN field_name = " . db_qstr($field_name) . ' THEN field_value ELSE NULL END) AS ' . db_qstr($field_name);
}
}
@@ -2273,7 +2274,7 @@ function make_host_snnp_cache_sql() : string|false {
$sql = "\t\tSELECT host_id ";
foreach ($fields as $field) {
$sql .= ",\n\t\t\tMAX(CASE WHEN field_name = '{$field['field_name']}' THEN field_value ELSE NULL END) AS `{$field['field_name']}`";
$sql .= ",\n\t\t\tMAX(CASE WHEN field_name = " . db_qstr($field['field_name']) . ' THEN field_value ELSE NULL END) AS ' . db_qstr($field['field_name']);
}
$sql .= "\n\t\t\tFROM host_snmp_cache AS hsc GROUP BY host_id";
@@ -3223,7 +3224,7 @@ function create_dq_graphs(int $host_id, int $snmp_query_id, array $rule) : bool
if (cacti_sizeof($field_names) > 0) {
foreach ($field_names as $column) {
$sql_query .= ", MAX(CASE WHEN field_name ='$column' THEN field_value ELSE NULL END) AS '$column'";
$sql_query .= ', MAX(CASE WHEN field_name = ' . db_qstr($column) . ' THEN field_value ELSE NULL END) AS ' . db_qstr($column);
$i++;
}
}
+7 -6
View File
@@ -1100,16 +1100,17 @@ function api_device_save(int $id, int $device_template_id, string $description,
$save['snmp_options'] = form_input_validate($snmp_options, 'snmp_options', '^[0-9]+$', false, 3);
$save['snmp_version'] = form_input_validate($snmp_version, 'snmp_version', '', true, 3);
$save['snmp_community'] = form_input_validate($snmp_community, 'snmp_community', '', true, 3);
// GHSA-m3fh-gxqj-76hq: SNMP credentials reach the net-snmp command line; strip control characters the same way snmp_community's sibling requires.
$save['snmp_community'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) form_input_validate($snmp_community, 'snmp_community', '', true, 3));
if ($save['snmp_version'] == 3) {
$save['snmp_username'] = form_input_validate($snmp_username, 'snmp_username', '', true, 3);
$save['snmp_password'] = form_input_validate($snmp_password, 'snmp_password', '', true, 3);
$save['snmp_username'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) form_input_validate($snmp_username, 'snmp_username', '', true, 3));
$save['snmp_password'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) form_input_validate($snmp_password, 'snmp_password', '', true, 3));
$save['snmp_auth_protocol'] = form_input_validate($snmp_auth_protocol, 'snmp_auth_protocol', "^(\[None\]|MD5|SHA|SHA224|SHA256|SHA384|SHA512)$", true, 3);
$save['snmp_priv_passphrase'] = form_input_validate($snmp_priv_passphrase, 'snmp_priv_passphrase', '', true, 3);
$save['snmp_priv_passphrase'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) form_input_validate($snmp_priv_passphrase, 'snmp_priv_passphrase', '', true, 3));
$save['snmp_priv_protocol'] = form_input_validate($snmp_priv_protocol, 'snmp_priv_protocol', "^(\[None\]|DES|AES|AES128|AES192|AES192C|AES256|AES256C)$", true, 3);
$save['snmp_context'] = form_input_validate($snmp_context, 'snmp_context', '', true, 3);
$save['snmp_engine_id'] = form_input_validate($snmp_engine_id, 'snmp_engine_id', '', true, 3);
$save['snmp_context'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) form_input_validate($snmp_context, 'snmp_context', '', true, 3));
$save['snmp_engine_id'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) form_input_validate($snmp_engine_id, 'snmp_engine_id', '', true, 3));
if (strlen($save['snmp_password']) < 8 && $snmp_auth_protocol != '[None]') {
raise_message(32);
+3 -1
View File
@@ -170,10 +170,12 @@ function check_auth_cookie() : int|false {
if (cacti_sizeof($user_info)) {
$secret = hash('sha512', $token, false);
// GHSA-xq26-f282-f998: reject remember-me tokens past the 90-day retention window instead of trusting the cron purge alone.
$found = db_fetch_cell_prepared('SELECT user_id
FROM user_auth_cache
WHERE user_id = ?
AND token = ?',
AND token = ?
AND last_update >= DATE_SUB(NOW(), INTERVAL 90 DAY)',
[$user_info['id'], $secret]
);
+2 -1
View File
@@ -1496,7 +1496,8 @@ function db_column_exists(string $table, string $column, bool $log = true, mixed
return $results[$index][$table][$column];
}
$results[$index][$table][$column] = (db_fetch_cell("SHOW columns FROM `$table` LIKE '$column'", '', $log, $db_conn) ? true : false);
// GHSA-rp5g-r5vp-q7j6: this helper validates attacker-supplied identifiers, so its own LIKE term must be quoted on the same connection that runs the query.
$results[$index][$table][$column] = (db_fetch_cell("SHOW columns FROM `$table` LIKE " . db_qstr($column, $db_conn), '', $log, $db_conn) ? true : false);
return $results[$index][$table][$column];
}
+24 -5
View File
@@ -1693,14 +1693,33 @@ function xml_to_data_query(string $hash, array &$xml_array, array &$hash_cache,
}
if (isset($save['xml_path'])) {
$path = str_replace('<path_cacti>', CACTI_PATH_BASE, $save['xml_path']);
$path = str_replace('<path_cacti>', CACTI_PATH_BASE, (string) $save['xml_path']);
if (!file_exists($path)) {
// GHSA-m67r-fcmw-gvv7: confine the imported path to the Cacti tree so it cannot probe arbitrary files.
// Resolve only the trusted base; a lexical prefix/traversal check gates the attacker value so
// realpath() never touches a UNC/remote path (which on Windows would initiate SMB access) before containment is proven.
$allowed_base = realpath(CACTI_PATH_BASE);
$base_norm = ($allowed_base !== false) ? str_replace('\\', '/', $allowed_base) : false;
$path_norm = str_replace('\\', '/', $path);
$lexically_contained = ($base_norm !== false
&& strpos($path_norm, $base_norm . '/') === 0
&& !str_contains($path_norm, '/../')
&& !str_ends_with($path_norm, '/..'));
if (!$lexically_contained) {
$files[$path] = 'missing';
} elseif (!is_readable($path)) {
$files[$path] = 'notreadable';
} else {
$files[$path] = 'found';
// Candidate is lexically inside the tree; canonicalize now to catch symlink escapes.
$resolved = realpath($path);
if ($resolved === false || strpos(str_replace('\\', '/', $resolved) . '/', $base_norm . '/') !== 0) {
$files[$path] = 'missing';
} elseif (!is_readable($resolved)) {
$files[$path] = 'notreadable';
} else {
$files[$path] = 'found';
}
}
}
+23 -2
View File
@@ -462,6 +462,13 @@ function api_plugin_status_run(string $hook, array $required_capabilities, strin
function api_plugin_db_table_create(string $plugin, string $table, array $data) : void {
include_once(CACTI_PATH_LIBRARY . '/database.php');
// GHSA-h5wg-qf6f-7f6r: reject an unsafe DDL identifier outright; normalizing it could silently collide with an unrelated table.
if ($table === '' || !preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
cacti_log("ERROR: Refusing to create plugin table with invalid identifier '$table'", false, 'PLUGIN');
return;
}
$result = db_fetch_assoc('SHOW TABLES');
$tables = [];
@@ -572,7 +579,14 @@ function api_plugin_db_table_create(string $plugin, string $table, array $data)
}
function api_plugin_drop_table(string $table) : void {
db_execute("DROP TABLE IF EXISTS $table");
// GHSA-h5wg-qf6f-7f6r: reject an unsafe identifier outright so a crafted name cannot extend the DROP across other tables or collide after normalization.
if ($table === '' || !preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
cacti_log("ERROR: Refusing to drop plugin table with invalid identifier '$table'", false, 'PLUGIN');
return;
}
db_execute('DROP TABLE IF EXISTS `' . $table . '`');
api_plugin_drop_remote_table($table);
}
@@ -1030,6 +1044,13 @@ function api_plugin_replicate_config() : void {
function api_plugin_drop_remote_table(string $table) : void {
include_once(CACTI_PATH_LIBRARY . '/poller.php');
// GHSA-h5wg-qf6f-7f6r: self-protect so no caller can push an unsafe identifier to the remote pollers.
if ($table === '' || !preg_match('/^[a-zA-Z0-9_]+$/', $table)) {
cacti_log("ERROR: Refusing to drop remote plugin table with invalid identifier '$table'", false, 'PLUGIN');
return;
}
$gone_time = read_config_option('poller_interval') * 2;
$pollers = array_rekey(
@@ -1047,7 +1068,7 @@ function api_plugin_drop_remote_table(string $table) : void {
$rcnn_id = poller_connect_to_remote($poller_id);
if ($rcnn_id !== false) {
db_execute("DROP TABLE IF EXISTS $table", false, $rcnn_id);
db_execute('DROP TABLE IF EXISTS `' . $table . '`', false, $rcnn_id);
}
}
}
+2 -1
View File
@@ -5580,7 +5580,8 @@ function rrdtool_create_error_image(string $string, mixed $width = '', mixed $he
$font_file = 'C:/Windows/Fonts/Arial.ttf';
}
$themefile = CACTI_PATH_INCLUDE . '/themes/' . get_selected_theme() . '/rrdtheme.php';
// GHSA-mpfm-qhvj-5879: get_selected_theme() bypasses the theme allowlist, so validate it before it builds an include path.
$themefile = CACTI_PATH_INCLUDE . '/themes/' . cacti_validate_theme(get_selected_theme()) . '/rrdtheme.php';
if (file_exists($themefile) && is_readable($themefile)) {
include($themefile);
@@ -0,0 +1,25 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* Remember-me session hardening: the nopassword change path must revoke the
* server-side remember-me row, not just the browser cookie (GHSA-cg45), and
* the remember-me token check must enforce a retention window instead of
* trusting the cron purge alone (GHSA-xq26).
*/
$root = dirname(__DIR__, 4);
test('nopassword change revokes the server-side remember-me row (GHSA-cg45)', function () use ($root) {
$s = file_get_contents($root . '/auth_changepassword.php');
expect($s)->toContain('clear_auth_cookie()');
});
test('remember-me token check enforces a retention window (GHSA-xq26)', function () use ($root) {
$s = file_get_contents($root . '/lib/auth.php');
expect($s)->toContain('AND last_update >= DATE_SUB(NOW(), INTERVAL 90 DAY)');
});
@@ -0,0 +1,38 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* Path and RRD hardening: the imported data-query xml_path is confined to the
* Cacti tree (GHSA-m67r), rrdtool_create_error_image routes the theme through
* the allowlist (GHSA-mpfm), path_dsstats_log is escaped before the dsstats
* shell redirect (GHSA-pjq5), and the data_templates rrd_maximum/rrd_minimum
* validator is anchored (GHSA-wp33).
*/
$root = dirname(__DIR__, 4);
test('imported data-query xml_path is confined to the Cacti tree (GHSA-m67r)', function () use ($root) {
$s = file_get_contents($root . '/lib/import.php');
expect($s)->toContain('realpath(CACTI_PATH_BASE)')
->and($s)->toContain('$lexically_contained');
});
test('rrdtool_create_error_image validates the theme (GHSA-mpfm)', function () use ($root) {
$s = file_get_contents($root . '/lib/rrd.php');
expect($s)->toContain('cacti_validate_theme(get_selected_theme())');
});
test('path_dsstats_log is escaped before the shell redirect (GHSA-pjq5)', function () use ($root) {
$s = file_get_contents($root . '/lib/dsstats.php');
expect($s)->toContain(">> ' . cacti_escapeshellarg");
});
test('data_templates rrd_maximum/rrd_minimum validator is anchored (GHSA-wp33)', function () use ($root) {
$s = file_get_contents($root . '/data_templates.php');
expect($s)->toContain("'rrd_maximum', [new Assert")
->and($s)->toContain('\z/');
});
@@ -0,0 +1,22 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* GHSA-m3fh-gxqj-76hq: the SNMPv3 credential device fields (sibling of the
* snmp_community newline defect) must strip control characters before the
* values reach the net-snmp command line.
*/
$root = dirname(__DIR__, 4);
test('SNMPv3 credential fields strip control characters (GHSA-m3fh)', function () use ($root) {
$s = file_get_contents($root . '/lib/api_device.php');
expect($s)->toContain('(string) form_input_validate($snmp_password')
->and($s)->toContain('(string) form_input_validate($snmp_priv_passphrase')
->and($s)->toContain('(string) form_input_validate($snmp_community');
});
@@ -0,0 +1,62 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* SQL-injection hardening regressions. Each assertion pins a
* sibling-of-already-fixed SQLi sink: db_column_exists (GHSA-rp5g), the
* automation pivot builders (GHSA-vx2m), aggregate_make_sql_where (GHSA-929h),
* cli/add_graphs REGEXP (GHSA-mfw8), the plugin DDL identifiers (GHSA-h5wg),
* the user_admin sort_column pivot (GHSA-m49v), and the already-bound group
* filter (GHSA-wfcf).
*/
$root = dirname(__DIR__, 4);
test('db_column_exists quotes its LIKE term on the query connection (GHSA-rp5g)', function () use ($root) {
$s = file_get_contents($root . '/lib/database.php');
expect($s)->toContain('db_qstr($column, $db_conn)')
->and($s)->not->toContain('LIKE \'$column\'');
});
test('automation pivot builders escape field_name (GHSA-vx2m)', function () use ($root) {
$s = file_get_contents($root . '/lib/api_automation.php');
// The leading literal's quote style varies (strings with \n\t stay double-quoted, others are single),
// so count both forms of the db_qstr()-escaped pivot.
$escaped = substr_count($s, 'CASE WHEN field_name = " . db_qstr(')
+ substr_count($s, "CASE WHEN field_name = ' . db_qstr(");
expect($s)->not->toContain("CASE WHEN field_name='")
->and($s)->not->toContain("CASE WHEN field_name ='")
->and($escaped)->toBeGreaterThanOrEqual(3);
});
test('aggregate LIKE filter term is quoted (GHSA-929h)', function () use ($root) {
$s = file_get_contents($root . '/aggregate_graphs.php');
expect($s)->toContain("db_qstr('%'")
->and($s)->not->toContain("LIKE '%\" . trim(\$i)");
});
test('cli add_graphs uses db_qstr not addslashes for REGEXP (GHSA-mfw8)', function () use ($root) {
$s = file_get_contents($root . '/cli/add_graphs.php');
expect($s)->not->toContain('addslashes');
});
test('plugin drop/create confine the table identifier (GHSA-h5wg)', function () use ($root) {
$s = file_get_contents($root . '/lib/plugins.php');
expect($s)->not->toContain('"DROP TABLE IF EXISTS $table"')
->and($s)->toContain('!preg_match(\'/^[a-zA-Z0-9_]+$/\', $table)');
});
test('user_admin clamps sort_column to displayed columns (GHSA-m49v)', function () use ($root) {
$s = file_get_contents($root . '/user_admin.php');
expect($s)->toContain("set_request_var('sort_column', 'username')");
});
test('user_admin group filter is a bound parameter (GHSA-wfcf)', function () use ($root) {
$s = file_get_contents($root . '/user_admin.php');
expect($s)->toContain('ug.group_id = ?');
});
@@ -0,0 +1,46 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
*/
/*
* XSS hardening regressions. The automation Resulting Branch cell is the one
* live fix (GHSA-f7jw); the rest pin name sinks the broad develop XSS hardening
* already closed: reports device-description status (GHSA-4gph), graph export
* legend (GHSA-977w), Reports Data Query name (GHSA-mgcq), die_html_input_error
* echo (GHSA-5p79), and the aggregate rfilter attribute (GHSA-cfhh).
*/
$root = dirname(__DIR__, 4);
test('automation tree-rule preview escapes the Resulting Branch values (GHSA-f7jw)', function () use ($root) {
$s = file_get_contents($root . '/lib/api_automation.php');
expect($s)->toContain('htmle((string) array_shift($replacement))');
});
test('report-add device description status is escaped (GHSA-4gph)', function () use ($root) {
$s = file_get_contents($root . '/lib/reports.php');
expect($s)->not->toContain("__('Device \\'%s\\' successfully added to Report.'");
});
test('graph export legend columns are html-escaped (GHSA-977w)', function () use ($root) {
$s = file_get_contents($root . '/graph_xport.php');
expect($s)->toContain('htmle($xport_array');
});
test('Reports Data Query name is html-escaped (GHSA-mgcq)', function () use ($root) {
$s = file_get_contents($root . '/lib/reports.php');
expect($s)->toContain('htmle($data_query');
});
test('die_html_input_error escapes the echoed variable and value (GHSA-5p79)', function () use ($root) {
$s = file_get_contents($root . '/lib/html_validate.php');
expect($s)->toContain("\$func = CACTI_CLI ? 'trim' : 'htmle';");
});
test('aggregate rfilter is not echoed into a raw attribute (GHSA-cfhh)', function () use ($root) {
$s = file_get_contents($root . '/aggregate_graphs.php');
expect($s)->not->toContain("value='<?php print grv('rfilter')");
});
+1 -1
View File
@@ -31,7 +31,7 @@
"lib/auth.php": 2,
"lib/boost.php": 3,
"lib/data_query.php": 2,
"lib/database.php": 9,
"lib/database.php": 8,
"lib/dsstats.php": 2,
"lib/functions.php": 1,
"lib/html_form.php": 2,
+23
View File
@@ -2224,6 +2224,29 @@ function user() : void {
$sql_where",
$sql_params);
// GHSA-m49v-hr7h-wwcj: keep every sort key on a displayed column so ORDER BY cannot pivot onto user_auth.password/locked/tfa_secret.
// update_order_string() has already merged the request value into $_SESSION['sort_data'], so validate the stored keys too,
// not just the current request, and clear both session entries (data + string) if any key is disallowed.
$allowed_sort = ['username', 'id', 'full_name', 'enabled', 'realm', 'policy_graphs', 'policy_hosts', 'policy_graph_templates', 'dtime'];
$order_page = get_order_string_page(false);
$sort_ok = in_array(grv('sort_column'), $allowed_sort, true);
if ($sort_ok && isset($_SESSION['sort_data'][$order_page]) && is_array($_SESSION['sort_data'][$order_page])) {
foreach (array_keys($_SESSION['sort_data'][$order_page]) as $stored_column) {
if (!in_array($stored_column, $allowed_sort, true)) {
$sort_ok = false;
break;
}
}
}
if (!$sort_ok) {
set_request_var('sort_column', 'username');
unset($_SESSION['sort_data'][$order_page]);
unset($_SESSION['sort_string'][$order_page]);
}
$sql_order = get_order_string();
$sql_limit = ' LIMIT ' . ($rows * (grv('page') - 1)) . ',' . $rows;