Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "cacti/plugin_flowview",
"description": "plugin_flowview plugin for Cacti",
"license": "GPL-2.0-or-later",
"require-dev": {
"pestphp/pest": "^1.23"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"autoload-dev": {
"files": [
"tests/bootstrap.php"
]
}
}
40 changes: 20 additions & 20 deletions database.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ function flowview_db_close(&$flowview_cnn) {
* @return '1' for success, '0' for error
*/
function flowview_db_execute($sql, $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_execute($sql, $log, $flowview_cnn);
}
Expand All @@ -80,7 +80,7 @@ function flowview_db_execute($sql, $log = true, $cnn_id = false) {
* @return '1' for success, '0' for error
*/
function flowview_db_execute_prepared($sql, $parms = array(), $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_execute_prepared($sql, $parms, $log, $flowview_cnn);
}
Expand All @@ -97,7 +97,7 @@ function flowview_db_execute_prepared($sql, $parms = array(), $log = true, $cnn_
* @return (bool) the output of the sql query as a single variable
*/
function flowview_db_fetch_cell($sql, $col_name = '', $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_fetch_cell($sql, $col_name, $log, $flowview_cnn);
}
Expand All @@ -115,7 +115,7 @@ function flowview_db_fetch_cell($sql, $col_name = '', $log = true, $cnn_id = fal
* @return (bool) the output of the sql query as a single variable
*/
function flowview_db_fetch_cell_prepared($sql, $params = array(), $col_name = '', $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_fetch_cell_prepared($sql, $params, $col_name, $log, $flowview_cnn);
}
Expand All @@ -130,7 +130,7 @@ function flowview_db_fetch_cell_prepared($sql, $params = array(), $col_name = ''
* @return the first row of the result as a hash
*/
function flowview_db_fetch_row($sql, $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_fetch_row($sql, $log, $flowview_cnn);
}
Expand All @@ -146,7 +146,7 @@ function flowview_db_fetch_row($sql, $log = true, $cnn_id = false) {
* @return the first row of the result as a hash
*/
function flowview_db_fetch_row_prepared($sql, $params = array(), $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_fetch_row_prepared($sql, $params, $log, $flowview_cnn);
}
Expand All @@ -161,7 +161,7 @@ function flowview_db_fetch_row_prepared($sql, $params = array(), $log = true, $c
* @return the entire result set as a multi-dimensional hash
*/
function flowview_db_fetch_assoc($sql, $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_fetch_assoc($sql, $log, $flowview_cnn);
}
Expand All @@ -177,7 +177,7 @@ function flowview_db_fetch_assoc($sql, $log = true, $cnn_id = false) {
* @return the entire result set as a multi-dimensional hash
*/
function flowview_db_fetch_assoc_prepared($sql, $params = array(), $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_fetch_assoc_prepared($sql, $params, $log, $flowview_cnn);
}
Expand All @@ -190,7 +190,7 @@ function flowview_db_fetch_assoc_prepared($sql, $params = array(), $log = true,
* @return the id of the last auto incriment row that was created
*/
function flowview_db_fetch_insert_id($cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_fetch_insert_id($flowview_cnn);
}
Expand All @@ -206,7 +206,7 @@ function flowview_db_fetch_insert_id($cnn_id = false) {
* @return the auto incriment id column (if applicable)
*/
function flowview_db_replace($table_name, $array_items, $keyCols, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_replace($table_name, $array_items, $keyCols, $flowview_cnn);
}
Expand All @@ -223,7 +223,7 @@ function flowview_db_replace($table_name, $array_items, $keyCols, $cnn_id = fals
* @return the auto incriment id column (if applicable)
*/
function flowview_sql_save($array_items, $table_name, $key_cols = 'id', $autoinc = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return sql_save($array_items, $table_name, $key_cols, $autoinc, $flowview_cnn);
}
Expand All @@ -238,7 +238,7 @@ function flowview_sql_save($array_items, $table_name, $key_cols = 'id', $autoinc
* @return (bool) the output of the sql query as a single variable
*/
function flowview_db_table_exists($table, $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

preg_match("/([`]{0,1}(?<database>[\w_]+)[`]{0,1}\.){0,1}[`]{0,1}(?<table>[\w_]+)[`]{0,1}/", $table, $matches);
if ($matches !== false && array_key_exists('table', $matches)) {
Expand All @@ -250,7 +250,7 @@ function flowview_db_table_exists($table, $log = true, $cnn_id = false) {
}

function flowview_db_table_create($table, $data, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

$result = flowview_db_fetch_assoc('SHOW TABLES');
$tables = array();
Expand Down Expand Up @@ -352,13 +352,13 @@ function flowview_db_table_create($table, $data, $cnn_id = false) {
}

function flowview_db_column_exists($table, $column, $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_column_exists($table, $column, $log, $flowview_cnn);
}

function flowview_db_add_column($table, $column, $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_add_column($table, $column, $log, $flowview_cnn);
}
Expand All @@ -372,9 +372,9 @@ function flowview_db_add_column($table, $column, $log = true, $cnn_id = false) {
* or false on error
*/
function flowview_db_affected_rows($cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_affected_rows($flowview_cnn);;
return db_affected_rows($flowview_cnn);
}

/**
Expand All @@ -388,7 +388,7 @@ function flowview_db_affected_rows($cnn_id = false) {
* @return bool The output of the sql query as a single variable
*/
function flowview_db_index_exists($table, $index, $log = true, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

return db_index_exists($table, $index, $log, $flowview_cnn);
}
Expand All @@ -412,13 +412,13 @@ function flowview_get_connection($cnn_id) {
* @return (array) An array of column types indexed by the column names
*/
function flowview_db_get_table_column_types($table, $cnn_id = false) {
$flowview_cnn = flowview_get_connection($cnn_id);;
$flowview_cnn = flowview_get_connection($cnn_id);

$columns = db_fetch_assoc("SHOW COLUMNS FROM $table", false, $flowview_cnn);
$cols = array();
if (cacti_sizeof($columns)) {
foreach($columns as $col) {
$cols[$col['Field']] = array('type' => $col['Type'], 'null' => $col['Null'], 'default' => $col['Default'], 'extra' => $col['Extra']);;
$cols[$col['Field']] = array('type' => $col['Type'], 'null' => $col['Null'], 'default' => $col['Default'], 'extra' => $col['Extra']);
}
}

Expand Down
45 changes: 33 additions & 12 deletions flowview_devices.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
include('./include/auth.php');
include_once($config['base_path'] . '/plugins/flowview/setup.php');
include_once($config['base_path'] . '/plugins/flowview/functions.php');
include_once($config['base_path'] . '/plugins/flowview/flowview_security.php');

flowview_connect();

Expand Down Expand Up @@ -292,13 +293,23 @@ function save_device() {
$save['cmethod'] = get_nfilter_request_var('cmethod');
$save['bind_address'] = get_filter_request_var('bind_address', FILTER_VALIDATE_IP);
$save['allowfrom'] = get_filter_request_var('allowfrom', FILTER_VALIDATE_REGEXP, array('options' => array('regexp' => '/^([0-9\.\/ ,]+)$/')));
$save['port'] = get_filter_request_var('port');
$save['port'] = flowview_normalize_listener_port(get_nfilter_request_var('port'));
$save['protocol'] = get_nfilter_request_var('protocol');
$save['enabled'] = isset_request_var('enabled') ? 'on':'';

if ($save['port'] === false) {
raise_message(2, __('The listener port must be a numeric value between 1 and 65535.', 'flowview'), MESSAGE_LEVEL_ERROR);

header('Location: flowview_devices.php?header=false&action=edit&id=' . get_request_var('id'));
exit;
}

$id = flowview_sql_save($save, 'plugin_flowview_devices', 'id', true);

$pid = db_fetch_cell('SELECT pid FROM processes WHERE tasktype="flowview" AND taskname="master"');
$pid = db_fetch_cell_prepared('SELECT pid
FROM processes
WHERE tasktype = ?
AND taskname = ?', array('flowview', 'master'));

if (is_error_message()) {
raise_message(2);
Expand All @@ -320,10 +331,10 @@ function save_device() {
}

function restart_services() {
$pid = db_fetch_cell('SELECT pid
$pid = db_fetch_cell_prepared('SELECT pid
FROM processes
WHERE tasktype="flowview"
AND taskname="master"');
WHERE tasktype = ?
AND taskname = ?', array('flowview', 'master'));

if ($pid > 0) {
if (!defined('SIGHUP')) {
Expand Down Expand Up @@ -887,18 +898,29 @@ function clearFilter() {

if (cacti_sizeof($result)) {
foreach ($result as $row) {
$status = '';
$parts = array();

if ($os == 'freebsd') {
$status = shell_exec("netstat -an | grep '." . $row['port'] . " '");
$status_cmd = flowview_build_listener_status_command($os, $row['port']);
$column = 3;
$scolumn = -1;
} else {
$status = shell_exec("ss -lntu | grep ':" . $row['port'] . " '");
$status_cmd = flowview_build_listener_status_command($os, $row['port']);
$column = 4;
$scolumn = 2;
if (empty($status)) {
$status = shell_exec("netstat -an | grep ':" . $row['port'] . " '");
$column = 3;
$scolumn = 1;
}

if ($status_cmd !== false) {
$status = shell_exec($status_cmd);

if ($os != 'freebsd' && empty($status)) {
$status_cmd = flowview_build_listener_status_command($os, $row['port'], true);
if ($status_cmd !== false) {
$status = shell_exec($status_cmd);
$column = 3;
$scolumn = 1;
}
}
}

Expand Down Expand Up @@ -974,4 +996,3 @@ function clearFilter() {

form_end();
}

42 changes: 42 additions & 0 deletions flowview_security.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

function flowview_normalize_listener_port($value) {
if (is_int($value)) {
$port = $value;
} elseif (is_string($value) && preg_match('/^[0-9]+$/', $value)) {
$port = (int) $value;
} else {
return false;
}

if ($port < 1 || $port > 65535) {
return false;
}

return $port;
}

function flowview_build_listener_status_command($os, $port, $use_fallback = false) {
$port = flowview_normalize_listener_port($port);

if ($port === false) {
return false;
}

if ($os == 'freebsd') {
return "netstat -an | grep '." . $port . " '";
}

if ($use_fallback) {
return "netstat -an | grep ':" . $port . " '";
}

return "ss -lntu | grep ':" . $port . " '";
}
10 changes: 6 additions & 4 deletions setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ function plugin_flowview_check_upgrade($force = false) {
$info = plugin_flowview_version();
$current = $info['version'];

$old = db_fetch_cell('SELECT version
$old = db_fetch_cell_prepared('SELECT version
FROM plugin_config
WHERE directory="flowview"');
WHERE directory = ?', array('flowview'));

if ($current != $old || $force) {
$php_binary = read_config_option('path_php_binary');
Expand Down Expand Up @@ -321,7 +321,10 @@ function flowview_global_settings_update() {
}

if ($hup_process) {
$pid = db_fetch_cell('SELECT pid FROM processes WHERE tasktype="flowview" AND taskname="master"');
$pid = db_fetch_cell_prepared('SELECT pid
FROM processes
WHERE tasktype = ?
AND taskname = ?', array('flowview', 'master'));

if ($pid > 0) {
if (!defined('SIGHUP')) {
Expand Down Expand Up @@ -1309,4 +1312,3 @@ function flowview_graph_button($data) {
}
}
}

22 changes: 22 additions & 0 deletions tests/E2E/ListenerPortSecurityRegressionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

describe('listener port security regression wiring', function () {
it('does not leave raw row port interpolation in shell_exec calls', function () {
$path = realpath(__DIR__ . '/../../flowview_devices.php');
expect($path)->not->toBeFalse();

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse();

expect($contents)->not->toContain("shell_exec(\"netstat -an | grep '.\" . \$row['port'] . \" '\")");
expect($contents)->not->toContain("shell_exec(\"ss -lntu | grep ':\" . \$row['port'] . \" '\")");
expect($contents)->toContain("\$status_cmd = flowview_build_listener_status_command(\$os, \$row['port'])");
});
});
24 changes: 24 additions & 0 deletions tests/Integration/ListenerStatusCommandSecurityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

describe('listener status command security', function () {
it('validates ports before saving and before shell execution', function () {
$path = realpath(__DIR__ . '/../../flowview_devices.php');
expect($path)->not->toBeFalse();

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse();

expect($contents)->toContain("include_once(\$config['base_path'] . '/plugins/flowview/flowview_security.php');");
expect($contents)->toContain("\$save['port'] = flowview_normalize_listener_port(get_nfilter_request_var('port'));");
expect($contents)->toContain("if (\$save['port'] === false)");
expect($contents)->toContain("flowview_build_listener_status_command(\$os, \$row['port'])");
expect($contents)->toContain("flowview_build_listener_status_command(\$os, \$row['port'], true)");
});
});
Loading