mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
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:
1 parent
5e870a91df
commit
9facd9b500
19 files changed
+310
-25
No files matched your search
@@ -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.
|
- 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
|
## 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 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)`.
|
- 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`).
|
- Run poller: `php poller.php --poller=1 --force --debug` (daemon debug: `./cactid.php --foreground --debug`).
|
||||||
|
|||||||
@@ -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-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-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-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-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-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
|
-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
|
||||||
|
|||||||
@@ -1839,7 +1839,7 @@ function aggregate_make_sql_where(string $sql_where, array $items, string $field
|
|||||||
} elseif (cacti_strtolower($i) == 'or') {
|
} elseif (cacti_strtolower($i) == 'or') {
|
||||||
$sql_where .= ' OR ';
|
$sql_where .= ' OR ';
|
||||||
} else {
|
} else {
|
||||||
$sql_where .= ($termcount > 0 ? ' OR ' : '') . $field . " LIKE '%" . trim($i) . "%'";
|
$sql_where .= ($termcount > 0 ? ' OR ' : '') . $field . ' LIKE ' . db_qstr('%' . trim($i) . '%');
|
||||||
$termcount++;
|
$termcount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ if ($user['password_change'] != 'on') {
|
|||||||
|
|
||||||
// destroy session information
|
// destroy session information
|
||||||
kill_session_var(SESS_USER_ID);
|
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();
|
cacti_cookie_logout();
|
||||||
|
|
||||||
$return = validate_redirect_url($_SERVER['HTTP_REFERER'] ?? '', 'index.php');
|
$return = validate_redirect_url($_SERVER['HTTP_REFERER'] ?? '', 'index.php');
|
||||||
|
|||||||
+2
-2
@@ -741,7 +741,7 @@ if (cacti_sizeof($parms)) {
|
|||||||
if (isset($dsGraph['snmpValue'][$index_snmp_filter])) {
|
if (isset($dsGraph['snmpValue'][$index_snmp_filter])) {
|
||||||
$req .= ' AND field_value = ' . db_qstr($dsGraph['snmpValue'][$index_snmp_filter]) . ')';
|
$req .= ' AND field_value = ' . db_qstr($dsGraph['snmpValue'][$index_snmp_filter]) . ')';
|
||||||
} elseif (isset($dsGraph['snmpValueRegex'][$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++;
|
$index_snmp_filter++;
|
||||||
@@ -754,7 +754,7 @@ if (cacti_sizeof($parms)) {
|
|||||||
foreach ($dsGraph['snmpFieldExclude'] as $snmpField) {
|
foreach ($dsGraph['snmpFieldExclude'] as $snmpField) {
|
||||||
$req .= ' AND snmp_index NOT IN (
|
$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);
|
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++;
|
$index_snmp_filter++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1271,9 +1271,10 @@ function display_matching_trees(int $rule_id, int $rule_type, array $item, strin
|
|||||||
for ($j = 0; cacti_sizeof($replacement); $j++) {
|
for ($j = 0; cacti_sizeof($replacement); $j++) {
|
||||||
if ($j > 0) {
|
if ($j > 0) {
|
||||||
$repl .= '<br>';
|
$repl .= '<br>';
|
||||||
$repl .= str_pad('', $j * 3, '-') . ' ' . 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, '-') . ' ' . htmle((string) array_shift($replacement));
|
||||||
} else {
|
} 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) {
|
if (cacti_sizeof($field_names) > 0) {
|
||||||
foreach (($field_names ?: []) as $column) {
|
foreach (($field_names ?: []) as $column) {
|
||||||
$field_name = $column['field_name'];
|
$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 ";
|
$sql = "\t\tSELECT host_id ";
|
||||||
|
|
||||||
foreach ($fields as $field) {
|
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";
|
$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) {
|
if (cacti_sizeof($field_names) > 0) {
|
||||||
foreach ($field_names as $column) {
|
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++;
|
$i++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -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_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_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) {
|
if ($save['snmp_version'] == 3) {
|
||||||
$save['snmp_username'] = form_input_validate($snmp_username, 'snmp_username', '', true, 3);
|
$save['snmp_username'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) form_input_validate($snmp_username, 'snmp_username', '', true, 3));
|
||||||
$save['snmp_password'] = form_input_validate($snmp_password, 'snmp_password', '', 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_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_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_context'] = preg_replace('/[\x00-\x1f\x7f]/', '', (string) 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_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]') {
|
if (strlen($save['snmp_password']) < 8 && $snmp_auth_protocol != '[None]') {
|
||||||
raise_message(32);
|
raise_message(32);
|
||||||
|
|||||||
+3
-1
@@ -170,10 +170,12 @@ function check_auth_cookie() : int|false {
|
|||||||
if (cacti_sizeof($user_info)) {
|
if (cacti_sizeof($user_info)) {
|
||||||
$secret = hash('sha512', $token, false);
|
$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
|
$found = db_fetch_cell_prepared('SELECT user_id
|
||||||
FROM user_auth_cache
|
FROM user_auth_cache
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND token = ?',
|
AND token = ?
|
||||||
|
AND last_update >= DATE_SUB(NOW(), INTERVAL 90 DAY)',
|
||||||
[$user_info['id'], $secret]
|
[$user_info['id'], $secret]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1496,7 +1496,8 @@ function db_column_exists(string $table, string $column, bool $log = true, mixed
|
|||||||
return $results[$index][$table][$column];
|
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];
|
return $results[$index][$table][$column];
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-5
@@ -1693,14 +1693,33 @@ function xml_to_data_query(string $hash, array &$xml_array, array &$hash_cache,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($save['xml_path'])) {
|
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';
|
$files[$path] = 'missing';
|
||||||
} elseif (!is_readable($path)) {
|
|
||||||
$files[$path] = 'notreadable';
|
|
||||||
} else {
|
} 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
@@ -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 {
|
function api_plugin_db_table_create(string $plugin, string $table, array $data) : void {
|
||||||
include_once(CACTI_PATH_LIBRARY . '/database.php');
|
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');
|
$result = db_fetch_assoc('SHOW TABLES');
|
||||||
$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 {
|
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);
|
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 {
|
function api_plugin_drop_remote_table(string $table) : void {
|
||||||
include_once(CACTI_PATH_LIBRARY . '/poller.php');
|
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;
|
$gone_time = read_config_option('poller_interval') * 2;
|
||||||
|
|
||||||
$pollers = array_rekey(
|
$pollers = array_rekey(
|
||||||
@@ -1047,7 +1068,7 @@ function api_plugin_drop_remote_table(string $table) : void {
|
|||||||
$rcnn_id = poller_connect_to_remote($poller_id);
|
$rcnn_id = poller_connect_to_remote($poller_id);
|
||||||
|
|
||||||
if ($rcnn_id !== false) {
|
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
@@ -5580,7 +5580,8 @@ function rrdtool_create_error_image(string $string, mixed $width = '', mixed $he
|
|||||||
$font_file = 'C:/Windows/Fonts/Arial.ttf';
|
$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)) {
|
if (file_exists($themefile) && is_readable($themefile)) {
|
||||||
include($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')");
|
||||||
|
});
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"lib/auth.php": 2,
|
"lib/auth.php": 2,
|
||||||
"lib/boost.php": 3,
|
"lib/boost.php": 3,
|
||||||
"lib/data_query.php": 2,
|
"lib/data_query.php": 2,
|
||||||
"lib/database.php": 9,
|
"lib/database.php": 8,
|
||||||
"lib/dsstats.php": 2,
|
"lib/dsstats.php": 2,
|
||||||
"lib/functions.php": 1,
|
"lib/functions.php": 1,
|
||||||
"lib/html_form.php": 2,
|
"lib/html_form.php": 2,
|
||||||
|
|||||||
@@ -2224,6 +2224,29 @@ function user() : void {
|
|||||||
$sql_where",
|
$sql_where",
|
||||||
$sql_params);
|
$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_order = get_order_string();
|
||||||
$sql_limit = ' LIMIT ' . ($rows * (grv('page') - 1)) . ',' . $rows;
|
$sql_limit = ' LIMIT ' . ($rows * (grv('page') - 1)) . ',' . $rows;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user