From ad5065dfb38f5b5c668b33241f8107363e073db2 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 02:48:49 -0700 Subject: [PATCH 01/11] harden(mactrack): escape shell args in passthru command construction Signed-off-by: Thomas Vincent --- lib/mactrack_functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index a70dfca..b93bf5b 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -3145,7 +3145,7 @@ function mactrack_rescan($web = false) { ob_start(); // execute the command, and show the results - $command = read_config_option('path_php_binary') . ' -q ' . $command_string . $extra_args; + $command = cacti_escapeshellarg(read_config_option('path_php_binary')) . ' -q ' . cacti_escapeshellarg($command_string) . $extra_args; passthru($command); $data['content'] = ob_get_clean(); @@ -3184,7 +3184,7 @@ function mactrack_site_scan($web = false) { ob_start(); // execute the command, and show the results - $command = read_config_option('path_php_binary') . ' -q ' . $command_string . $extra_args; + $command = cacti_escapeshellarg(read_config_option('path_php_binary')) . ' -q ' . cacti_escapeshellarg($command_string) . $extra_args; passthru($command); $data['content'] = ob_get_clean(); From 029766bea2d8750fccb3e4b3f333dafff21314e9 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 02:51:45 -0700 Subject: [PATCH 02/11] harden(mactrack): escape extra_args tokens and capture passthru exit code Signed-off-by: Thomas Vincent --- lib/mactrack_functions.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index b93bf5b..6bb0a59 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -3135,7 +3135,7 @@ function mactrack_rescan($web = false) { // create the command script $command_string = $config['base_path'] . '/plugins/mactrack/mactrack_scanner.php'; - $extra_args = ' -id=' . $dbinfo['device_id'] . ($web ? ' --web' : ''); + $extra_args = ' -id=' . cacti_escapeshellarg($dbinfo['device_id']) . ($web ? ' --web' : ''); // print out the type, and device_id $data['device_id'] = get_request_var('device_id'); @@ -3146,9 +3146,13 @@ function mactrack_rescan($web = false) { // execute the command, and show the results $command = cacti_escapeshellarg(read_config_option('path_php_binary')) . ' -q ' . cacti_escapeshellarg($command_string) . $extra_args; - passthru($command); + passthru($command, $exit_code); $data['content'] = ob_get_clean(); + + if ($exit_code !== 0) { + $data['error'] = 'rescan process exited with code ' . intval($exit_code); + } } } @@ -3175,7 +3179,7 @@ function mactrack_site_scan($web = false) { // create the command script $command_string = $config['base_path'] . '/plugins/mactrack/poller_mactrack.php'; - $extra_args = ' --web -sid=' . $dbinfo['site_id']; + $extra_args = ' --web -sid=' . cacti_escapeshellarg($dbinfo['site_id']); // print out the type, and device_id $data['site_id'] = $site_id; @@ -3185,9 +3189,13 @@ function mactrack_site_scan($web = false) { // execute the command, and show the results $command = cacti_escapeshellarg(read_config_option('path_php_binary')) . ' -q ' . cacti_escapeshellarg($command_string) . $extra_args; - passthru($command); + passthru($command, $exit_code); $data['content'] = ob_get_clean(); + + if ($exit_code !== 0) { + $data['error'] = 'site_scan process exited with code ' . intval($exit_code); + } } header('Content-Type: application/json; charset=utf-8'); From 7a3dcce74051b43db147d795f61af0ab03d1420e Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 02:58:39 -0700 Subject: [PATCH 03/11] harden(mactrack): validate request vars before AJAX shell dispatch; remove global clobber Signed-off-by: Thomas Vincent --- lib/mactrack_functions.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 6bb0a59..51a519e 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -3118,6 +3118,9 @@ function mactrack_display_Octets($octets) { function mactrack_rescan($web = false) { global $config; + get_filter_request_var('device_id'); + get_filter_request_var('ifIndex'); + $device_id = get_request_var('device_id'); $ifIndex = get_request_var('ifIndex'); @@ -3162,7 +3165,9 @@ function mactrack_rescan($web = false) { } function mactrack_site_scan($web = false) { - global $config, $web; + global $config; + + get_filter_request_var('site_id'); $site_id = get_request_var('site_id'); From ac8178ab953ba77ec6661742dc025c60040ce623 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 03:22:27 -0700 Subject: [PATCH 04/11] hardening(mactrack): cherry-pick prepared stmts and XSS fixes from #327 - db_fetch_assoc_prepared for interfaces query in mactrack_functions.php - db_fetch_row_prepared for host/mt_device lookups in mactrack_actions.php - html_escape device_name/hostname in mactrack_devices.php - unserialize allowed_classes guard + html_escape in mactrack_view_macs.php Signed-off-by: Thomas Vincent --- lib/mactrack_functions.php | 2 +- mactrack_actions.php | 4 ++-- mactrack_devices.php | 2 +- mactrack_view_macs.php | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 51a519e..791ac7c 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -573,7 +573,7 @@ function build_InterfacesTable(&$device, &$ifIndexes, $getLinkPorts = false, $ge } // required only for interfaces table - $db_data = db_fetch_assoc("SELECT * FROM mac_track_interfaces WHERE device_id='" . $device['device_id'] . "' ORDER BY ifIndex"); + $db_data = db_fetch_assoc_prepared('SELECT * FROM mac_track_interfaces WHERE device_id = ? ORDER BY ifIndex', [$device['device_id']]); if (cacti_sizeof($db_data)) { foreach ($db_data as $interface) { diff --git a/mactrack_actions.php b/mactrack_actions.php index ab253dd..751affa 100644 --- a/mactrack_actions.php +++ b/mactrack_actions.php @@ -147,7 +147,7 @@ function sync_mactrack_to_cacti($mt_device) { $mt_device['snmp_engine_id'] ??= ''; // fetch current data for cacti device - $cacti_device = db_fetch_row('SELECT * FROM host WHERE id=' . $mt_device['host_id']); + $cacti_device = db_fetch_row_prepared('SELECT * FROM host WHERE id = ?', [$mt_device['host_id']]); if (cacti_sizeof($cacti_device)) { // update cacti device @@ -176,7 +176,7 @@ function sync_cacti_to_mactrack($device) { if ((read_config_option('mt_update_policy', true) == 2) && ($device['id'] > 0)) { // $devices holds the whole row from host table // now fetch the related device from mac_track_devices, if any - $mt_device = db_fetch_row('SELECT * from mac_track_devices WHERE host_id=' . $device['id']); + $mt_device = db_fetch_row_prepared('SELECT * FROM mac_track_devices WHERE host_id = ?', [$device['id']]); if (is_array($mt_device) && $mt_device) { $mt_device['snmp_engine_id'] ??= ''; diff --git a/mactrack_devices.php b/mactrack_devices.php index 5c895ad..378714a 100644 --- a/mactrack_devices.php +++ b/mactrack_devices.php @@ -879,7 +879,7 @@ function mactrack_device_edit() { diff --git a/mactrack_view_macs.php b/mactrack_view_macs.php index bfcbc36..4131bf9 100644 --- a/mactrack_view_macs.php +++ b/mactrack_view_macs.php @@ -88,7 +88,7 @@ function form_actions() { // if we are to save this form, instead of display it if (isset_request_var('selected_items')) { - $selected_items = unserialize(get_nfilter_request_var('selected_items'), ['allowed_classes' => false]); + $selected_items = unserialize(get_nfilter_request_var('selected_items'), ['allowed_classes' => false]); // nosemgrep: php.lang.security.unserialize-use.unserialize-use -- allowed_classes mitigates gadget chaining; MAC/IP values validated below if (!is_array($selected_items)) { header('Location: mactrack_view_macs.php'); @@ -152,7 +152,7 @@ function form_actions() { } if (!isset($mac_address_array[$mac])) { - $mac_address_list .= '
  • ' . mactrack_format_mac($mac) . '
  • '; + $mac_address_list .= '
  • ' . html_escape(mactrack_format_mac($mac)) . '
  • '; $mac_address_array[$mac] = $ip; } } @@ -193,7 +193,7 @@ function form_actions() { print " @@ -221,7 +221,8 @@ function form_aggregated_actions() { if ($selected_items != false) { if (get_request_var('drp_action') == '3') { // Delete if (cacti_sizeof($selected_items)) { - db_execute('DELETE FROM mac_track_aggregated_ports WHERE row_id IN (' . implode(',', $selected_items) . ')'); + $placeholders = implode(',', array_fill(0, cacti_sizeof($selected_items), '?')); + db_execute_prepared('DELETE FROM mac_track_aggregated_ports WHERE row_id IN (' . $placeholders . ')', $selected_items); } } @@ -250,9 +251,10 @@ function form_aggregated_actions() { if (cacti_sizeof($row_array)) { $row_ids = implode(',', $row_array); - $rows_info = db_fetch_assoc('SELECT device_name, mac_address, ip_address, port_number, count_rec + $row_placeholders = implode(',', array_fill(0, cacti_sizeof($row_array), '?')); + $rows_info = db_fetch_assoc_prepared('SELECT device_name, mac_address, ip_address, port_number, count_rec FROM mac_track_aggregated_ports - WHERE row_id IN (' . implode(',', $row_array) . ')'); + WHERE row_id IN (' . $row_placeholders . ')', $row_array); if (isset($rows_info)) { foreach ($rows_info as $row_info) { @@ -289,8 +291,8 @@ function form_aggregated_actions() { print " From 58327394c8ce6820a93c02722a69936399d4fe8e Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 02:13:11 -0700 Subject: [PATCH 08/11] hardening(mactrack): fix stored XSS and second-order SQLi in mactrack_devices.php form_mactrack_actions(): html_escape() the device_name/hostname printed into the confirmation
  • list. These values originate from SNMP discovery (e.g. sysName) and were emitted unescaped (stored XSS). Mirrors the escaping already applied to mactrack_device_edit(). Copy SNMP Settings (drp_action==7): the UPDATE built its SET clause by interpolating host-table values with bare single quotes, allowing second-order SQL injection when a value contains a quote. Values are now bound via db_execute_prepared() placeholders; the column name ($field_name) remains interpolated as it is a trusted key from the static $fields_mactrack_snmp_item definition. Co-Authored-By: Oz --- mactrack_devices.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/mactrack_devices.php b/mactrack_devices.php index 378714a..47207c9 100644 --- a/mactrack_devices.php +++ b/mactrack_devices.php @@ -202,19 +202,26 @@ function form_mactrack_actions() { if (isset($cacti_host['id'])) { reset($fields_mactrack_snmp_item); - $updates = ''; + $set_clauses = []; + $set_params = []; foreach ($fields_mactrack_snmp_item as $field_name => $field_array) { if (isset($cacti_host[$field_name])) { - $updates .= ($updates != '' ? ', ' : '') . $field_name . "='" . $cacti_host[$field_name] . "'"; + // $field_name is a trusted key from the static $fields_mactrack_snmp_item + // definition; the host-table value is bound as a parameter to avoid + // second-order SQL injection when it contains a quote. + $set_clauses[] = $field_name . ' = ?'; + $set_params[] = $cacti_host[$field_name]; } } - if ($updates != '') { + if (cacti_sizeof($set_clauses)) { + $set_params[] = $selected_items[$i]; + db_execute_prepared('UPDATE mac_track_devices - SET ' . $updates . ' - WHERE device_id=?', - [$selected_items[$i]]); + SET ' . implode(', ', $set_clauses) . ' + WHERE device_id = ?', + $set_params); } } else { // skip silently; possible enhancement: tell the user what we did @@ -246,7 +253,7 @@ function form_mactrack_actions() { FROM mac_track_devices WHERE device_id = ?', [$matches[1]]); - $device_list .= '
  • ' . $device_info['device_name'] . ' (' . $device_info['hostname'] . ')
  • '; + $device_list .= '
  • ' . html_escape($device_info['device_name']) . ' (' . html_escape($device_info['hostname']) . ')
  • '; $device_array[$i] = $matches[1]; $i++; } From 3719f294dda8eb076bd50f111b250824e23e5669 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Tue, 14 Jul 2026 02:37:26 -0700 Subject: [PATCH 09/11] Use runner PHP without versioned Apache package --- .github/workflows/plugin-ci-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index bdbd889..d0936cf 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -85,7 +85,7 @@ jobs: run: sudo apt-get update - name: Install System Dependencies - run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping libapache2-mod-php${{ matrix.php }} + run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping - name: Start SNMPD Agent and Test run: | From d5177caffa4a57cc102b59303992bbc44536adfe Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Tue, 14 Jul 2026 02:40:43 -0700 Subject: [PATCH 10/11] Apply project PHP formatting --- mactrack_view_macs.php | 4 ++-- tests/Pest.php | 4 +--- tests/Security/Php74CompatibilityTest.php | 4 ++-- .../PreparedStatementConsistencyTest.php | 8 +++---- tests/Security/SetupStructureTest.php | 4 +--- tests/bootstrap.php | 24 ++++++++++--------- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/mactrack_view_macs.php b/mactrack_view_macs.php index 2f7cc35..a11efeb 100644 --- a/mactrack_view_macs.php +++ b/mactrack_view_macs.php @@ -250,9 +250,9 @@ function form_aggregated_actions() { } if (cacti_sizeof($row_array)) { - $row_ids = implode(',', $row_array); + $row_ids = implode(',', $row_array); $row_placeholders = implode(',', array_fill(0, cacti_sizeof($row_array), '?')); - $rows_info = db_fetch_assoc_prepared('SELECT device_name, mac_address, ip_address, port_number, count_rec + $rows_info = db_fetch_assoc_prepared('SELECT device_name, mac_address, ip_address, port_number, count_rec FROM mac_track_aggregated_ports WHERE row_id IN (' . $row_placeholders . ')', $row_array); diff --git a/tests/Pest.php b/tests/Pest.php index e2132a3..675e214 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -7,8 +7,6 @@ +-------------------------------------------------------------------------+ */ -/* - * Pest configuration file. - */ +// Pest configuration file. require_once __DIR__ . '/bootstrap.php'; diff --git a/tests/Security/Php74CompatibilityTest.php b/tests/Security/Php74CompatibilityTest.php index 644ce4f..2b20810 100644 --- a/tests/Security/Php74CompatibilityTest.php +++ b/tests/Security/Php74CompatibilityTest.php @@ -13,7 +13,7 @@ */ describe('PHP 7.4 compatibility in mactrack', function () { - $files = array( + $files = [ 'mactrack_devices.php', 'mactrack_device_types.php', 'mactrack_interfaces.php', @@ -24,7 +24,7 @@ 'mactrack_view_macs.php', 'mactrack_view_sites.php', 'setup.php', - ); + ]; it('does not use str_contains (PHP 8.0)', function () use ($files) { foreach ($files as $relativeFile) { diff --git a/tests/Security/PreparedStatementConsistencyTest.php b/tests/Security/PreparedStatementConsistencyTest.php index 5e3e471..8c74477 100644 --- a/tests/Security/PreparedStatementConsistencyTest.php +++ b/tests/Security/PreparedStatementConsistencyTest.php @@ -14,7 +14,7 @@ describe('prepared statement consistency in mactrack', function () { it('uses prepared DB helpers in all plugin files', function () { - $targetFiles = array( + $targetFiles = [ 'mactrack_devices.php', 'mactrack_device_types.php', 'mactrack_interfaces.php', @@ -25,9 +25,9 @@ 'mactrack_view_macs.php', 'mactrack_view_sites.php', 'setup.php', - ); + ]; - $rawPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)\s*\(/'; + $rawPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)\s*\(/'; $preparedPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)_prepared\s*\(/'; foreach ($targetFiles as $relativeFile) { @@ -43,7 +43,7 @@ continue; } - $lines = explode("\n", $contents); + $lines = explode("\n", $contents); $rawCallsOutsideComments = 0; foreach ($lines as $line) { diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php index e789db3..83b7148 100644 --- a/tests/Security/SetupStructureTest.php +++ b/tests/Security/SetupStructureTest.php @@ -7,9 +7,7 @@ +-------------------------------------------------------------------------+ */ -/* - * Verify setup.php defines required plugin hooks and info function. - */ +// Verify setup.php defines required plugin hooks and info function. describe('mactrack setup.php structure', function () { $source = file_get_contents(realpath(__DIR__ . '/../../setup.php')); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 0aa22f2..bb20bc6 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,43 +12,45 @@ * can be loaded in isolation without the full Cacti application. */ -$GLOBALS['__test_db_calls'] = array(); +$GLOBALS['__test_db_calls'] = []; if (!function_exists('db_execute')) { function db_execute($sql) { - $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute', 'sql' => $sql, 'params' => array()); + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_execute', 'sql' => $sql, 'params' => []]; + return true; } } if (!function_exists('db_execute_prepared')) { - function db_execute_prepared($sql, $params = array()) { - $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params); + function db_execute_prepared($sql, $params = []) { + $GLOBALS['__test_db_calls'][] = ['fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params]; + return true; } } if (!function_exists('db_fetch_assoc')) { function db_fetch_assoc($sql) { - return array(); + return []; } } if (!function_exists('db_fetch_assoc_prepared')) { - function db_fetch_assoc_prepared($sql, $params = array()) { - return array(); + function db_fetch_assoc_prepared($sql, $params = []) { + return []; } } if (!function_exists('db_fetch_row')) { function db_fetch_row($sql) { - return array(); + return []; } } if (!function_exists('db_fetch_row_prepared')) { - function db_fetch_row_prepared($sql, $params = array()) { - return array(); + function db_fetch_row_prepared($sql, $params = []) { + return []; } } @@ -59,7 +61,7 @@ function db_fetch_cell($sql) { } if (!function_exists('db_fetch_cell_prepared')) { - function db_fetch_cell_prepared($sql, $params = array()) { + function db_fetch_cell_prepared($sql, $params = []) { return ''; } } From 92633daba5bc505dd9f9c2917cbb3ee825a55b64 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Tue, 14 Jul 2026 03:28:56 -0700 Subject: [PATCH 11/11] fix: escape aggregated MAC confirm list fields Device name, IP, MAC, and port values are echoed into HTML lists. Signed-off-by: Thomas Vincent --- mactrack_view_macs.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mactrack_view_macs.php b/mactrack_view_macs.php index a11efeb..9045cb3 100644 --- a/mactrack_view_macs.php +++ b/mactrack_view_macs.php @@ -236,7 +236,6 @@ function form_aggregated_actions() { $mac_address_list = ''; $row_list = ''; $i = 0; - $row_ids = ''; // loop through each of the ports selected on the previous page and get more info about them foreach ($_POST as $var => $val) { @@ -250,7 +249,6 @@ function form_aggregated_actions() { } if (cacti_sizeof($row_array)) { - $row_ids = implode(',', $row_array); $row_placeholders = implode(',', array_fill(0, cacti_sizeof($row_array), '?')); $rows_info = db_fetch_assoc_prepared('SELECT device_name, mac_address, ip_address, port_number, count_rec FROM mac_track_aggregated_ports @@ -258,7 +256,13 @@ function form_aggregated_actions() { if (isset($rows_info)) { foreach ($rows_info as $row_info) { - $row_list .= '
  • ' . __('Dev.:%s IP.:%s MAC.:%s PORT.:%s Count.: [%s]', $row_info['device_name'], $row_info['ip_address'], mactrack_format_mac($row_info['mac_address']), $row_info['port_number'], $row_info['count_rec'], 'mactrack') . '
  • '; + $row_list .= '
  • ' . __('Dev.:%s IP.:%s MAC.:%s PORT.:%s Count.: [%s]', + html_escape($row_info['device_name']), + html_escape($row_info['ip_address']), + html_escape(mactrack_format_mac($row_info['mac_address'])), + html_escape((string) $row_info['port_number']), + html_escape((string) $row_info['count_rec']), + 'mactrack') . '
  • '; } } }
    - () + ()
    - + " . ($save_html != '' ? " $save_html" : "') . ' From 799ca2ed29c324c96576758d96e9551a6b78cebc Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 03:23:42 -0700 Subject: [PATCH 05/11] test: add Pest v1 security test scaffold from #328 Signed-off-by: Thomas Vincent --- tests/Pest.php | 14 ++ tests/Security/Php74CompatibilityTest.php | 108 ++++++++++ .../PreparedStatementConsistencyTest.php | 66 ++++++ tests/Security/SetupStructureTest.php | 36 ++++ tests/bootstrap.php | 200 ++++++++++++++++++ 5 files changed, 424 insertions(+) create mode 100644 tests/Pest.php create mode 100644 tests/Security/Php74CompatibilityTest.php create mode 100644 tests/Security/PreparedStatementConsistencyTest.php create mode 100644 tests/Security/SetupStructureTest.php create mode 100644 tests/bootstrap.php diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..e2132a3 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,14 @@ +toBe(0, + "{$relativeFile} uses str_contains() which requires PHP 8.0" + ); + } + }); + + it('does not use str_starts_with (PHP 8.0)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + continue; + } + + $contents = file_get_contents($path); + + if ($contents === false) { + continue; + } + + expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0, + "{$relativeFile} uses str_starts_with() which requires PHP 8.0" + ); + } + }); + + it('does not use str_ends_with (PHP 8.0)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + continue; + } + + $contents = file_get_contents($path); + + if ($contents === false) { + continue; + } + + expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0, + "{$relativeFile} uses str_ends_with() which requires PHP 8.0" + ); + } + }); + + it('does not use nullsafe operator (PHP 8.0)', function () use ($files) { + foreach ($files as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + + if ($path === false) { + continue; + } + + $contents = file_get_contents($path); + + if ($contents === false) { + continue; + } + + expect(preg_match('/\?->/', $contents))->toBe(0, + "{$relativeFile} uses nullsafe operator which requires PHP 8.0" + ); + } + }); +}); diff --git a/tests/Security/PreparedStatementConsistencyTest.php b/tests/Security/PreparedStatementConsistencyTest.php new file mode 100644 index 0000000..5e3e471 --- /dev/null +++ b/tests/Security/PreparedStatementConsistencyTest.php @@ -0,0 +1,66 @@ +toBe(0, + "File {$relativeFile} contains raw (unprepared) DB calls" + ); + } + }); +}); diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php new file mode 100644 index 0000000..e789db3 --- /dev/null +++ b/tests/Security/SetupStructureTest.php @@ -0,0 +1,36 @@ +toContain('function plugin_mactrack_install'); + }); + + it('defines plugin_mactrack_version function', function () use ($source) { + expect($source)->toContain('function plugin_mactrack_version'); + }); + + it('defines plugin_mactrack_uninstall function', function () use ($source) { + expect($source)->toContain('function plugin_mactrack_uninstall'); + }); + + it('returns version array with name key', function () use ($source) { + expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/'); + }); + + it('returns version array with version key', function () use ($source) { + expect($source)->toMatch('/[\'\""]version[\'\""]\s*=>/'); + }); +}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..0aa22f2 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,200 @@ + 'db_execute', 'sql' => $sql, 'params' => array()); + return true; + } +} + +if (!function_exists('db_execute_prepared')) { + function db_execute_prepared($sql, $params = array()) { + $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params); + return true; + } +} + +if (!function_exists('db_fetch_assoc')) { + function db_fetch_assoc($sql) { + return array(); + } +} + +if (!function_exists('db_fetch_assoc_prepared')) { + function db_fetch_assoc_prepared($sql, $params = array()) { + return array(); + } +} + +if (!function_exists('db_fetch_row')) { + function db_fetch_row($sql) { + return array(); + } +} + +if (!function_exists('db_fetch_row_prepared')) { + function db_fetch_row_prepared($sql, $params = array()) { + return array(); + } +} + +if (!function_exists('db_fetch_cell')) { + function db_fetch_cell($sql) { + return ''; + } +} + +if (!function_exists('db_fetch_cell_prepared')) { + function db_fetch_cell_prepared($sql, $params = array()) { + return ''; + } +} + +if (!function_exists('db_index_exists')) { + function db_index_exists($table, $index) { + return false; + } +} + +if (!function_exists('db_column_exists')) { + function db_column_exists($table, $column) { + return false; + } +} + +if (!function_exists('api_plugin_db_add_column')) { + function api_plugin_db_add_column($plugin, $table, $data) { + return true; + } +} + +if (!function_exists('api_plugin_db_table_create')) { + function api_plugin_db_table_create($plugin, $table, $data) { + return true; + } +} + +if (!function_exists('read_config_option')) { + function read_config_option($name, $force = false) { + return ''; + } +} + +if (!function_exists('set_config_option')) { + function set_config_option($name, $value) { + } +} + +if (!function_exists('html_escape')) { + function html_escape($string) { + return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } +} + +if (!function_exists('__')) { + function __($text, $domain = '') { + return $text; + } +} + +if (!function_exists('__esc')) { + function __esc($text, $domain = '') { + return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + } +} + +if (!function_exists('cacti_log')) { + function cacti_log($message, $also_print = false, $log_type = '', $level = 0) { + } +} + +if (!function_exists('cacti_sizeof')) { + function cacti_sizeof($array) { + return is_array($array) ? count($array) : 0; + } +} + +if (!function_exists('is_realm_allowed')) { + function is_realm_allowed($realm) { + return true; + } +} + +if (!function_exists('raise_message')) { + function raise_message($id, $text = '', $level = 0) { + } +} + +if (!function_exists('get_request_var')) { + function get_request_var($name) { + return ''; + } +} + +if (!function_exists('get_nfilter_request_var')) { + function get_nfilter_request_var($name) { + return ''; + } +} + +if (!function_exists('get_filter_request_var')) { + function get_filter_request_var($name) { + return ''; + } +} + +if (!function_exists('form_input_validate')) { + function form_input_validate($value, $name, $regex, $optional, $error) { + return $value; + } +} + +if (!function_exists('is_error_message')) { + function is_error_message() { + return false; + } +} + +if (!function_exists('sql_save')) { + function sql_save($array, $table, $key = 'id') { + return isset($array['id']) ? $array['id'] : 1; + } +} + +if (!defined('CACTI_PATH_BASE')) { + define('CACTI_PATH_BASE', '/var/www/html/cacti'); +} + +if (!defined('POLLER_VERBOSITY_LOW')) { + define('POLLER_VERBOSITY_LOW', 2); +} + +if (!defined('POLLER_VERBOSITY_MEDIUM')) { + define('POLLER_VERBOSITY_MEDIUM', 3); +} + +if (!defined('POLLER_VERBOSITY_DEBUG')) { + define('POLLER_VERBOSITY_DEBUG', 5); +} + +if (!defined('POLLER_VERBOSITY_NONE')) { + define('POLLER_VERBOSITY_NONE', 6); +} + +if (!defined('MESSAGE_LEVEL_ERROR')) { + define('MESSAGE_LEVEL_ERROR', 1); +} From 0c45b3e995688cbb68a1ad73d6513e6da83fcb7b Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 03:27:52 -0700 Subject: [PATCH 06/11] fix(mactrack): address Copilot review on site_scan --web flag and error surfacing Make --web conditional on $web param in mactrack_site_scan (was always added). Append subprocess exit-code errors to data['content'] so the AJAX client shows failures rather than an empty dialog. Signed-off-by: Thomas Vincent --- lib/mactrack_functions.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 791ac7c..4858be6 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -3154,7 +3154,8 @@ function mactrack_rescan($web = false) { $data['content'] = ob_get_clean(); if ($exit_code !== 0) { - $data['error'] = 'rescan process exited with code ' . intval($exit_code); + $data['error'] = 'rescan process exited with code ' . intval($exit_code); + $data['content'] .= '

    ' . html_escape('Subprocess error: exit code ' . intval($exit_code)) . '

    '; } } } @@ -3184,7 +3185,7 @@ function mactrack_site_scan($web = false) { // create the command script $command_string = $config['base_path'] . '/plugins/mactrack/poller_mactrack.php'; - $extra_args = ' --web -sid=' . cacti_escapeshellarg($dbinfo['site_id']); + $extra_args = ' -sid=' . cacti_escapeshellarg($dbinfo['site_id']) . ($web ? ' --web' : ''); // print out the type, and device_id $data['site_id'] = $site_id; @@ -3199,7 +3200,8 @@ function mactrack_site_scan($web = false) { $data['content'] = ob_get_clean(); if ($exit_code !== 0) { - $data['error'] = 'site_scan process exited with code ' . intval($exit_code); + $data['error'] = 'site_scan process exited with code ' . intval($exit_code); + $data['content'] .= '

    ' . html_escape('Subprocess error: exit code ' . intval($exit_code)) . '

    '; } } From bd015de868139c2924b07f97c73890450ce57835 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 01:50:43 -0700 Subject: [PATCH 07/11] hardening(mactrack): escape drp_action output and parameterize row_id IN() deletes mactrack_view_macs.php: wrap the drp_action hidden-input value with html_escape() in both form_actions() and form_aggregated_actions() (defense in depth; drp_action is already int-validated via get_filter_request_var), and escape the aggregated serialize($row_array) hidden input for parity with the MAC form. Parameterize the two interpolated 'row_id IN (...)' statements (aggregated delete and the row lookup) with db_execute_prepared()/db_fetch_assoc_prepared() and ?-placeholders. The bound values were already numeric-validated (sanitize_unserialize_selected_items / input_validate_input_number), so this is defense in depth and brings the file in line with the PR's PreparedStatementConsistencyTest. Co-Authored-By: Oz --- mactrack_view_macs.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mactrack_view_macs.php b/mactrack_view_macs.php index 4131bf9..2f7cc35 100644 --- a/mactrack_view_macs.php +++ b/mactrack_view_macs.php @@ -194,7 +194,7 @@ function form_actions() {
    - " . ($save_html != '' ? " + " . ($save_html != '' ? " $save_html" : "') . '
    - - " . ($save_html != '' ? " + + " . ($save_html != '' ? " $save_html" : "') . '