diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5e645dcbb..6f5ac6f2f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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`). diff --git a/CHANGELOG b/CHANGELOG index f45428c6a..a0c435c12 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 diff --git a/aggregate_graphs.php b/aggregate_graphs.php index b0ec44c2c..6c0c2c178 100644 --- a/aggregate_graphs.php +++ b/aggregate_graphs.php @@ -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++; } } diff --git a/auth_changepassword.php b/auth_changepassword.php index 1a132f83f..ce54727f5 100644 --- a/auth_changepassword.php +++ b/auth_changepassword.php @@ -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'); diff --git a/cli/add_graphs.php b/cli/add_graphs.php index 759538ad0..d5e92917b 100755 --- a/cli/add_graphs.php +++ b/cli/add_graphs.php @@ -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++; } } diff --git a/lib/api_automation.php b/lib/api_automation.php index 7e7d875da..e280697f5 100644 --- a/lib/api_automation.php +++ b/lib/api_automation.php @@ -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 .= '
'; - $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
layout. + $repl .= str_pad('', $j * 3, '-') . ' ' . 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++; } } diff --git a/lib/api_device.php b/lib/api_device.php index 8533c55a4..1f73229e1 100644 --- a/lib/api_device.php +++ b/lib/api_device.php @@ -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); diff --git a/lib/auth.php b/lib/auth.php index 6fc9c2012..3cb1cd450 100644 --- a/lib/auth.php +++ b/lib/auth.php @@ -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] ); diff --git a/lib/database.php b/lib/database.php index caf4db041..59408f493 100644 --- a/lib/database.php +++ b/lib/database.php @@ -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]; } diff --git a/lib/import.php b/lib/import.php index 0245324de..066c5cbc8 100644 --- a/lib/import.php +++ b/lib/import.php @@ -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('', CACTI_PATH_BASE, $save['xml_path']); + $path = str_replace('', 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'; + } } } diff --git a/lib/plugins.php b/lib/plugins.php index 06bbcd550..4a73cba91 100644 --- a/lib/plugins.php +++ b/lib/plugins.php @@ -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); } } } diff --git a/lib/rrd.php b/lib/rrd.php index d1b955826..9bca37010 100644 --- a/lib/rrd.php +++ b/lib/rrd.php @@ -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); diff --git a/tests/Unit/Security/Auth/RememberMeSessionHardeningTest.php b/tests/Unit/Security/Auth/RememberMeSessionHardeningTest.php new file mode 100644 index 000000000..5b4059199 --- /dev/null +++ b/tests/Unit/Security/Auth/RememberMeSessionHardeningTest.php @@ -0,0 +1,25 @@ +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)'); +}); diff --git a/tests/Unit/Security/PathTraversal/PathAndRrdHardeningTest.php b/tests/Unit/Security/PathTraversal/PathAndRrdHardeningTest.php new file mode 100644 index 000000000..6620369e8 --- /dev/null +++ b/tests/Unit/Security/PathTraversal/PathAndRrdHardeningTest.php @@ -0,0 +1,38 @@ +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/'); +}); diff --git a/tests/Unit/Security/Shell/SnmpControlCharacterValidationTest.php b/tests/Unit/Security/Shell/SnmpControlCharacterValidationTest.php new file mode 100644 index 000000000..700fac7e8 --- /dev/null +++ b/tests/Unit/Security/Shell/SnmpControlCharacterValidationTest.php @@ -0,0 +1,22 @@ +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'); +}); diff --git a/tests/Unit/Security/SqlInjection/SqlInjectionHardeningRegressionTest.php b/tests/Unit/Security/SqlInjection/SqlInjectionHardeningRegressionTest.php new file mode 100644 index 000000000..1ea379514 --- /dev/null +++ b/tests/Unit/Security/SqlInjection/SqlInjectionHardeningRegressionTest.php @@ -0,0 +1,62 @@ +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 = ?'); +}); diff --git a/tests/Unit/Security/Xss/XssHardeningRegressionTest.php b/tests/Unit/Security/Xss/XssHardeningRegressionTest.php new file mode 100644 index 000000000..103be5b76 --- /dev/null +++ b/tests/Unit/Security/Xss/XssHardeningRegressionTest.php @@ -0,0 +1,46 @@ +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='