Skip to content
31 changes: 23 additions & 8 deletions lib/mactrack_functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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');

Expand All @@ -3135,7 +3138,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');
Expand All @@ -3145,10 +3148,15 @@ 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;
passthru($command);
$command = cacti_escapeshellarg(read_config_option('path_php_binary')) . ' -q ' . cacti_escapeshellarg($command_string) . $extra_args;
passthru($command, $exit_code);

$data['content'] = ob_get_clean();

if ($exit_code !== 0) {
$data['error'] = 'rescan process exited with code ' . intval($exit_code);
$data['content'] .= '<p class="textError">' . html_escape('Subprocess error: exit code ' . intval($exit_code)) . '</p>';
}
Comment thread
somethingwithproof marked this conversation as resolved.
}
}

Expand All @@ -3158,7 +3166,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');

Expand All @@ -3175,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=' . $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;
Expand All @@ -3184,10 +3194,15 @@ 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;
passthru($command);
$command = cacti_escapeshellarg(read_config_option('path_php_binary')) . ' -q ' . cacti_escapeshellarg($command_string) . $extra_args;
passthru($command, $exit_code);

$data['content'] = ob_get_clean();

if ($exit_code !== 0) {
$data['error'] = 'site_scan process exited with code ' . intval($exit_code);
$data['content'] .= '<p class="textError">' . html_escape('Subprocess error: exit code ' . intval($exit_code)) . '</p>';
}
}

header('Content-Type: application/json; charset=utf-8');
Expand Down
4 changes: 2 additions & 2 deletions mactrack_actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'] ??= '';
Expand Down
2 changes: 1 addition & 1 deletion mactrack_devices.php
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ function mactrack_device_edit() {
<table class='cactiTable'>
<tr>
<td class='textInfo' colspan='2'>
<?php print $device['device_name']; ?> (<?php print $device['hostname']; ?>)
<?php print html_escape($device['device_name']); ?> (<?php print html_escape($device['hostname']); ?>)
</td>
</tr>
<tr>
Expand Down
6 changes: 3 additions & 3 deletions mactrack_view_macs.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -152,7 +152,7 @@ function form_actions() {
}

if (!isset($mac_address_array[$mac])) {
$mac_address_list .= '<li>' . mactrack_format_mac($mac) . '</li>';
$mac_address_list .= '<li>' . html_escape(mactrack_format_mac($mac)) . '</li>';
$mac_address_array[$mac] = $ip;
}
}
Expand Down Expand Up @@ -193,7 +193,7 @@ function form_actions() {
print "<tr>
<td colspan='2' class='saveRow'>
<input type='hidden' name='action' value='actions'>
<input type='hidden' name='selected_items' value='" . (isset($mac_address_array) ? serialize($mac_address_array) : '') . "'>
<input type='hidden' name='selected_items' value='" . (isset($mac_address_array) ? html_escape(serialize($mac_address_array)) : '') . "'>
<input type='hidden' name='drp_action' value='" . get_request_var('drp_action') . "'>" . ($save_html != '' ? "
<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Cancel', 'mactrack') . "</button>
$save_html" : "<button type='button' class='ui-button ui-corner-all ui-widget' onClick='cactiReturnTo()'>" . __esc('Return', 'mactrack') . '</button>') . '
Expand Down
14 changes: 14 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Pest configuration file.
*/

require_once __DIR__ . '/bootstrap.php';
108 changes: 108 additions & 0 deletions tests/Security/Php74CompatibilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify plugin source files do not use PHP 8.0+ syntax.
* Cacti 1.2.x plugins must remain compatible with PHP 7.4.
*/

describe('PHP 7.4 compatibility in mactrack', function () {
$files = array(
'mactrack_devices.php',
'mactrack_device_types.php',
'mactrack_interfaces.php',
'mactrack_sites.php',
'mactrack_snmp.php',
'mactrack_utilities.php',
'mactrack_view_arp.php',
'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) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}

expect(preg_match('/\bstr_contains\s*\(/', $contents))->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"
);
}
});
});
66 changes: 66 additions & 0 deletions tests/Security/PreparedStatementConsistencyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify migrated files use prepared DB helpers exclusively.
* Catches regressions where raw db_execute/db_fetch_* calls creep back in.
*/

describe('prepared statement consistency in mactrack', function () {
it('uses prepared DB helpers in all plugin files', function () {
$targetFiles = array(
'mactrack_devices.php',
'mactrack_device_types.php',
'mactrack_interfaces.php',
'mactrack_sites.php',
'mactrack_snmp.php',
'mactrack_utilities.php',
'mactrack_view_arp.php',
'mactrack_view_macs.php',
'mactrack_view_sites.php',
'setup.php',
);

$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) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}

$lines = explode("\n", $contents);
$rawCallsOutsideComments = 0;

foreach ($lines as $line) {
$trimmed = ltrim($line);

if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0 || strpos($trimmed, '#') === 0) {
continue;
}

if (preg_match($rawPattern, $line) && !preg_match($preparedPattern, $line)) {
$rawCallsOutsideComments++;
}
}

expect($rawCallsOutsideComments)->toBe(0,
"File {$relativeFile} contains raw (unprepared) DB calls"
);
}
});
});
36 changes: 36 additions & 0 deletions tests/Security/SetupStructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify setup.php defines required plugin hooks and info function.
*/

describe('mactrack setup.php structure', function () {
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));

it('defines plugin_mactrack_install function', function () use ($source) {
expect($source)->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*=>/');
});
});
Loading
Loading