From e435b2fc4008aeadaef0bceaa09a42ae96ce8eba Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 02:48:37 -0700 Subject: [PATCH 1/8] harden(weathermap): fix strict false comparison for strpos in target quoting Signed-off-by: Thomas Vincent --- lib/WeatherMapLink.class.php | 4 ++-- lib/WeatherMapNode.class.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/WeatherMapLink.class.php b/lib/WeatherMapLink.class.php index af3a1c2..da0fbd2 100644 --- a/lib/WeatherMapLink.class.php +++ b/lib/WeatherMapLink.class.php @@ -754,7 +754,7 @@ function WriteConfig() { $output .= TAB . 'TARGET'; foreach ($this->targets as $target) { - if (strpos($target[4], ' ') == false) { + if (strpos($target[4], ' ') === false) { $output .= ' ' . $target[4]; } else { $output .= ' "' . $target[4] . '"'; @@ -846,7 +846,7 @@ function asJS() { $tgt = ''; foreach ($this->targets as $target) { - if (strpos($target[4], ' ') == false) { + if (strpos($target[4], ' ') === false) { $tgt .= $target[4] . ' '; } else { $tgt .= '"' . $target[4] . '" '; diff --git a/lib/WeatherMapNode.class.php b/lib/WeatherMapNode.class.php index ab0614a..4866d4b 100644 --- a/lib/WeatherMapNode.class.php +++ b/lib/WeatherMapNode.class.php @@ -882,7 +882,7 @@ function WriteConfig() { $output .= TAB . 'TARGET'; foreach ($this->targets as $target) { - if (strpos($target[4], ' ') == false) { + if (strpos($target[4], ' ') === false) { $output .= ' ' . $target[4]; } else { $output .= ' "' . $target[4] . '"'; From 7ff275c44cacf65d12aa88b20f595e15bab43258 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 03:16:16 -0700 Subject: [PATCH 2/8] harden(weathermap): cacti_escapeshellarg for fping/rrd, html_escape maptitle, prepared cacti-mapper query Signed-off-by: Thomas Vincent --- cli/cacti-mapper.php | 5 ++-- .../WeatherMapDataSource_fping.php | 4 ++-- lib/datasources/WeatherMapDataSource_rrd.php | 24 ++++++++----------- weathermap-cacti-plugin.php | 2 +- 4 files changed, 16 insertions(+), 19 deletions(-) diff --git a/cli/cacti-mapper.php b/cli/cacti-mapper.php index c73bd25..bf3fd2b 100644 --- a/cli/cacti-mapper.php +++ b/cli/cacti-mapper.php @@ -58,7 +58,7 @@ WHERE hash = ?', [$data_template_hash]); -$queryrows = db_fetch_assoc("SELECT h.snmp_version, h.snmp_community, h.snmp_username, +$queryrows = db_fetch_assoc_prepared("SELECT h.snmp_version, h.snmp_community, h.snmp_username, h.snmp_password, h.snmp_auth_protocol, h.snmp_priv_passphrase, h.snmp_priv_protocol, h.snmp_context, h.snmp_port, h.snmp_timeout, h.description, h.hostname, h.disabled, hsc.* @@ -70,7 +70,8 @@ AND field_value != '127.0.0.1' AND field_value != '0.0.0.0' AND h.status = 3 - AND h.snmp_version > 0"); + AND h.snmp_version > 0", + array()); if (cacti_sizeof($queryrows)) { foreach ($queryrows as $line) { diff --git a/lib/datasources/WeatherMapDataSource_fping.php b/lib/datasources/WeatherMapDataSource_fping.php index 6540cff..9599710 100644 --- a/lib/datasources/WeatherMapDataSource_fping.php +++ b/lib/datasources/WeatherMapDataSource_fping.php @@ -100,10 +100,10 @@ function ReadData($targetstring, &$map, &$item) { $pattern .= '/'; if (is_executable($this->fping_cmd)) { - $command = $this->fping_cmd . " -t100 -r1 -p20 -u -C $ping_count -i10 -q $target 2>&1"; + $command = $this->fping_cmd . ' -t100 -r1 -p20 -u -C ' . (int) $ping_count . ' -i10 -q ' . cacti_escapeshellarg($target) . ' 2>&1'; // nosemgrep: php.lang.security.exec-use.exec-use -- fping_cmd is admin-configured; target validated against fping: pattern wm_debug("Running $command"); - $pipe = popen($command, 'r'); + $pipe = popen($command, 'r'); // nosemgrep: php.lang.security.exec-use.exec-use -- fping_cmd is admin-configured; target validated above via cacti_escapeshellarg $count = 0; $hitcount = 0; diff --git a/lib/datasources/WeatherMapDataSource_rrd.php b/lib/datasources/WeatherMapDataSource_rrd.php index 0aeffc3..5da288d 100644 --- a/lib/datasources/WeatherMapDataSource_rrd.php +++ b/lib/datasources/WeatherMapDataSource_rrd.php @@ -311,18 +311,16 @@ function wmrrd_read_from_real_rrdtool_aggregate($rrdfile,$cf,$aggregatefn,$start $command = $map->rrdtool; foreach ($args as $arg) { - if (strchr($arg, ' ') != false) { - $command .= ' "' . $arg . '"'; - } else { - $command .= ' ' . $arg; - } + $command .= ' ' . cacti_escapeshellarg($arg); } - $command .= ' ' . $extra_options; + foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { + $command .= ' ' . cacti_escapeshellarg($opt); + } wm_debug("RRD ReadData: Running: $command"); - $pipe = popen($command, 'r'); + $pipe = popen($command, 'r'); // nosemgrep: php.lang.security.exec-use.exec-use -- rrdtool path is admin-configured; all args cacti_escapeshellarg'd $lines = []; $count = 0; @@ -415,18 +413,16 @@ function wmrrd_read_from_real_rrdtool($rrdfile, $cf, $start, $end, $dsnames, &$d $command = $map->rrdtool; foreach ($args as $arg) { - if (strchr($arg, ' ') != false) { - $command .= ' "' . $arg . '"'; - } else { - $command .= ' ' . $arg; - } + $command .= ' ' . cacti_escapeshellarg($arg); } - $command .= ' ' . $extra_options; + foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { + $command .= ' ' . cacti_escapeshellarg($opt); + } wm_debug("RRD ReadData: Running: $command"); - $pipe = popen($command, 'r'); + $pipe = popen($command, 'r'); // nosemgrep: php.lang.security.exec-use.exec-use -- rrdtool path is admin-configured; all args cacti_escapeshellarg'd $lines = []; $count = 0; diff --git a/weathermap-cacti-plugin.php b/weathermap-cacti-plugin.php index f3f4faf..a26e42b 100644 --- a/weathermap-cacti-plugin.php +++ b/weathermap-cacti-plugin.php @@ -380,7 +380,7 @@ function weathermap_singleview($mapid) { print do_hook_function('weathermap_page_top', ''); $htmlfile = $outdir . $map['filehash'] . '.html'; - $maptitle = $map['titlecache']; + $maptitle = html_escape($map['titlecache']); if ($maptitle == '') { $maptitle = __esc('Map for config file: %s', $map['configfile']); From f4aea4b74442984f10b228dd6499061b3eb2b992 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 03:29:50 -0700 Subject: [PATCH 3/8] harden(weathermap): guard rrd_options against quoted/spaced arg corruption Reject extra_options containing quotes or backslashes with a wm_warn log entry rather than silently tokenizing flag=value pairs that contain spaces. Flags like --title "My Map" would otherwise become three malformed tokens. Documents that this field accepts only space-separated single-token flags. Signed-off-by: Thomas Vincent --- lib/datasources/WeatherMapDataSource_rrd.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/datasources/WeatherMapDataSource_rrd.php b/lib/datasources/WeatherMapDataSource_rrd.php index 5da288d..d1c2100 100644 --- a/lib/datasources/WeatherMapDataSource_rrd.php +++ b/lib/datasources/WeatherMapDataSource_rrd.php @@ -314,8 +314,14 @@ function wmrrd_read_from_real_rrdtool_aggregate($rrdfile,$cf,$aggregatefn,$start $command .= ' ' . cacti_escapeshellarg($arg); } - foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { - $command .= ' ' . cacti_escapeshellarg($opt); + if ($extra_options !== '' && $extra_options !== null) { + if (preg_match('/["\'\\]/', (string) $extra_options)) { + wm_warn('RRD ReadData: rrd_options contains quote or backslash characters and was skipped to prevent argument corruption. Use only space-separated single-token flags. [WMRRD04]'); + } else { + foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { + $command .= ' ' . cacti_escapeshellarg($opt); + } + } } wm_debug("RRD ReadData: Running: $command"); @@ -416,8 +422,14 @@ function wmrrd_read_from_real_rrdtool($rrdfile, $cf, $start, $end, $dsnames, &$d $command .= ' ' . cacti_escapeshellarg($arg); } - foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { - $command .= ' ' . cacti_escapeshellarg($opt); + if ($extra_options !== '' && $extra_options !== null) { + if (preg_match('/["\'\\]/', (string) $extra_options)) { + wm_warn('RRD ReadData: rrd_options contains quote or backslash characters and was skipped to prevent argument corruption. Use only space-separated single-token flags. [WMRRD04]'); + } else { + foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { + $command .= ' ' . cacti_escapeshellarg($opt); + } + } } wm_debug("RRD ReadData: Running: $command"); From 73bef58091f030a5062ab72136ca9beb3a57154b Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 03:58:10 -0700 Subject: [PATCH 4/8] harden(weathermap): escape rrdtool binary path in command construction Signed-off-by: Thomas Vincent --- lib/datasources/WeatherMapDataSource_rrd.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/datasources/WeatherMapDataSource_rrd.php b/lib/datasources/WeatherMapDataSource_rrd.php index d1c2100..f1c483e 100644 --- a/lib/datasources/WeatherMapDataSource_rrd.php +++ b/lib/datasources/WeatherMapDataSource_rrd.php @@ -308,7 +308,7 @@ function wmrrd_read_from_real_rrdtool_aggregate($rrdfile,$cf,$aggregatefn,$start $args[] = "PRINT:agg_out:'OUT %lf'"; } - $command = $map->rrdtool; + $command = cacti_escapeshellarg($map->rrdtool); foreach ($args as $arg) { $command .= ' ' . cacti_escapeshellarg($arg); @@ -416,7 +416,7 @@ function wmrrd_read_from_real_rrdtool($rrdfile, $cf, $start, $end, $dsnames, &$d $args[] = '--end'; $args[] = $end; - $command = $map->rrdtool; + $command = cacti_escapeshellarg($map->rrdtool); foreach ($args as $arg) { $command .= ' ' . cacti_escapeshellarg($arg); From 46cde866cae62377fc46ebb165dc52ec3be2eeea Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 04:03:18 -0700 Subject: [PATCH 5/8] harden(weathermap): escape fping binary path in command construction Signed-off-by: Thomas Vincent --- lib/datasources/WeatherMapDataSource_fping.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/datasources/WeatherMapDataSource_fping.php b/lib/datasources/WeatherMapDataSource_fping.php index 9599710..8ddb3cb 100644 --- a/lib/datasources/WeatherMapDataSource_fping.php +++ b/lib/datasources/WeatherMapDataSource_fping.php @@ -100,7 +100,7 @@ function ReadData($targetstring, &$map, &$item) { $pattern .= '/'; if (is_executable($this->fping_cmd)) { - $command = $this->fping_cmd . ' -t100 -r1 -p20 -u -C ' . (int) $ping_count . ' -i10 -q ' . cacti_escapeshellarg($target) . ' 2>&1'; // nosemgrep: php.lang.security.exec-use.exec-use -- fping_cmd is admin-configured; target validated against fping: pattern + $command = cacti_escapeshellarg($this->fping_cmd) . ' -t100 -r1 -p20 -u -C ' . (int) $ping_count . ' -i10 -q ' . cacti_escapeshellarg($target) . ' 2>&1'; // nosemgrep: php.lang.security.exec-use.exec-use -- fping_cmd is admin-configured; target validated against fping: pattern wm_debug("Running $command"); $pipe = popen($command, 'r'); // nosemgrep: php.lang.security.exec-use.exec-use -- fping_cmd is admin-configured; target validated above via cacti_escapeshellarg From ef49393f6664ea1548f186f146e6837c688e6a4f Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 17 May 2026 04:39:56 -0700 Subject: [PATCH 6/8] fix(hardening): db_fetch_cell_prepared, curvepoints empty guard, rrd_options log visibility setup.php: db_fetch_cell -> db_fetch_cell_prepared per project rule. WeatherMapLink: DrawComments and the post-draw label path both crash with TypeError on PHP 8 when curvepoints is empty. Add early return guard. WeatherMapDataSource_rrd: rrd_options rejection was silent at default verbosity. Supplement wm_warn with cacti_log at POLLER_VERBOSITY_LOW so operators see the skip without enabling debug logging. Signed-off-by: Thomas Vincent --- lib/WeatherMapLink.class.php | 8 ++++++++ lib/datasources/WeatherMapDataSource_rrd.php | 8 ++++++-- setup.php | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/WeatherMapLink.class.php b/lib/WeatherMapLink.class.php index da0fbd2..51e9641 100644 --- a/lib/WeatherMapLink.class.php +++ b/lib/WeatherMapLink.class.php @@ -244,6 +244,10 @@ function CopyFrom(&$source) { function DrawComments($image, $col, $widths) { $curvepoints = $this->curvepoints; + if (cacti_sizeof($curvepoints) === 0) { + return; + } + $last = cacti_count($curvepoints) - 1; $totaldistance = $curvepoints[$last][2]; @@ -515,6 +519,10 @@ function Draw($image, &$map) { $this->DrawComments($image,[$comment_colour_in, $comment_colour_out],[$link_in_width * 1.1, $link_out_width * 1.1]); } + if (cacti_sizeof($this->curvepoints) === 0) { + return; + } + $curvelength = $this->curvepoints[cacti_count($this->curvepoints) - 1][2]; // figure out where the labels should be, and what the angle of the curve is at that point diff --git a/lib/datasources/WeatherMapDataSource_rrd.php b/lib/datasources/WeatherMapDataSource_rrd.php index f1c483e..3b9d9a6 100644 --- a/lib/datasources/WeatherMapDataSource_rrd.php +++ b/lib/datasources/WeatherMapDataSource_rrd.php @@ -316,7 +316,9 @@ function wmrrd_read_from_real_rrdtool_aggregate($rrdfile,$cf,$aggregatefn,$start if ($extra_options !== '' && $extra_options !== null) { if (preg_match('/["\'\\]/', (string) $extra_options)) { - wm_warn('RRD ReadData: rrd_options contains quote or backslash characters and was skipped to prevent argument corruption. Use only space-separated single-token flags. [WMRRD04]'); + $msg = 'RRD ReadData: rrd_options contains quote or backslash characters and was skipped to prevent argument corruption. Use only space-separated single-token flags. [WMRRD04]'; + wm_warn($msg); + cacti_log('WEATHERMAP: ' . $msg, false, 'POLLER', POLLER_VERBOSITY_LOW); } else { foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { $command .= ' ' . cacti_escapeshellarg($opt); @@ -424,7 +426,9 @@ function wmrrd_read_from_real_rrdtool($rrdfile, $cf, $start, $end, $dsnames, &$d if ($extra_options !== '' && $extra_options !== null) { if (preg_match('/["\'\\]/', (string) $extra_options)) { - wm_warn('RRD ReadData: rrd_options contains quote or backslash characters and was skipped to prevent argument corruption. Use only space-separated single-token flags. [WMRRD04]'); + $msg = 'RRD ReadData: rrd_options contains quote or backslash characters and was skipped to prevent argument corruption. Use only space-separated single-token flags. [WMRRD04]'; + wm_warn($msg); + cacti_log('WEATHERMAP: ' . $msg, false, 'POLLER', POLLER_VERBOSITY_LOW); } else { foreach (preg_split('/\s+/', (string) $extra_options, -1, PREG_SPLIT_NO_EMPTY) as $opt) { $command .= ' ' . cacti_escapeshellarg($opt); diff --git a/setup.php b/setup.php index 9c0562c..30e6f0e 100644 --- a/setup.php +++ b/setup.php @@ -113,7 +113,7 @@ function plugin_weathermap_upgrade() { $current = plugin_weathermap_version(); $current = $current['version']; - $old = db_fetch_cell("SELECT version FROM plugin_config WHERE directory='weathermap'"); + $old = db_fetch_cell_prepared("SELECT version FROM plugin_config WHERE directory = ?", ['weathermap']); if ($current != $old) { db_execute_prepared('UPDATE plugin_realms From d46d86d9b0d32f77477e31205d69280140ef6a01 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Mon, 18 May 2026 13:15:14 -0400 Subject: [PATCH 7/8] Fix query to use db_fetch_assoc instead of db_fetch_assoc_prepared Signed-off-by: TheWitness --- cli/cacti-mapper.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/cacti-mapper.php b/cli/cacti-mapper.php index bf3fd2b..c73bd25 100644 --- a/cli/cacti-mapper.php +++ b/cli/cacti-mapper.php @@ -58,7 +58,7 @@ WHERE hash = ?', [$data_template_hash]); -$queryrows = db_fetch_assoc_prepared("SELECT h.snmp_version, h.snmp_community, h.snmp_username, +$queryrows = db_fetch_assoc("SELECT h.snmp_version, h.snmp_community, h.snmp_username, h.snmp_password, h.snmp_auth_protocol, h.snmp_priv_passphrase, h.snmp_priv_protocol, h.snmp_context, h.snmp_port, h.snmp_timeout, h.description, h.hostname, h.disabled, hsc.* @@ -70,8 +70,7 @@ AND field_value != '127.0.0.1' AND field_value != '0.0.0.0' AND h.status = 3 - AND h.snmp_version > 0", - array()); + AND h.snmp_version > 0"); if (cacti_sizeof($queryrows)) { foreach ($queryrows as $line) { From e1c34b38a36adf066ccaf4f5f038e72ba950205f Mon Sep 17 00:00:00 2001 From: TheWitness Date: Mon, 18 May 2026 13:16:11 -0400 Subject: [PATCH 8/8] Fix database query for fetching old version Signed-off-by: TheWitness --- setup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.php b/setup.php index 30e6f0e..30c7637 100644 --- a/setup.php +++ b/setup.php @@ -113,7 +113,7 @@ function plugin_weathermap_upgrade() { $current = plugin_weathermap_version(); $current = $current['version']; - $old = db_fetch_cell_prepared("SELECT version FROM plugin_config WHERE directory = ?", ['weathermap']); + $old = db_fetch_cell("SELECT version FROM plugin_config WHERE directory = 'weathermap'"); if ($current != $old) { db_execute_prepared('UPDATE plugin_realms