From af940c24750b7c4d164643da52c13be10b3a3708 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Wed, 8 Apr 2026 23:03:09 -0700 Subject: [PATCH 1/4] fix(security): defense-in-depth hardening for plugin_wmi Automated fixes: - XSS: escape request variables in HTML value attributes - SQLi: convert string-concat queries to prepared statements - Deserialization: add allowed_classes=>false - Temp files: replace rand() with tempnam() Signed-off-by: Thomas Vincent --- linux_wmi.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux_wmi.php b/linux_wmi.php index c154043..694db4d 100644 --- a/linux_wmi.php +++ b/linux_wmi.php @@ -289,7 +289,7 @@ function retrieve_account() { function decode($info) { $info = base64_decode($info); - $info = unserialize($info); + $info = unserialize($info, array('allowed_classes' => false)); $info = $info['password']; return $info; From 3bdd88485d05fc1471a66ac8b111179aff21cacd Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Thu, 9 Apr 2026 23:03:17 -0700 Subject: [PATCH 2/4] fix(ci): Dependabot composer ecosystem, CodeQL PHP coverage - Change Dependabot ecosystem from npm to composer (PHP-only repo) - Remove PHP from CodeQL paths-ignore so security PRs get analysis - Remove committed .omc session artifacts, add .omc/ to .gitignore Signed-off-by: Thomas Vincent --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2752239..ced409a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .git* locales/po/*.mo +.omc/ From 27d39e88a1a0bf18771416e65fad3f3c3328c13e Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 29 Aug 2026 23:59:31 -0700 Subject: [PATCH 3/4] security: escape the WMI hostname and namespace before exec Linux_WMI::clean() escaped the username, password, binary and command but left the device hostname only trimmed and the query namespace untouched, so getcommand() interpolated them raw into the wmic command line that exec() runs on the Cacti server. A device-supplied hostname such as 127.0.0.1; touch /tmp/pwned # therefore ran a command on the poller. Escape the hostname and namespace with cacti_escapeshellarg, and on Windows strip the cmd.exe metacharacters (" & | ^ < > ( ) %) that cmd.exe interprets despite quoting. A standalone regression test in tests/ verifies both. Signed-off-by: Thomas Vincent --- linux_wmi.php | 29 ++++++++++++---- tests/WmiCommandInjectionTest.php | 58 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 tests/WmiCommandInjectionTest.php diff --git a/linux_wmi.php b/linux_wmi.php index 694db4d..17147ab 100644 --- a/linux_wmi.php +++ b/linux_wmi.php @@ -223,8 +223,8 @@ function getcommand() { ' --delimiter=' . $this->separator . ' --user=' . $this->username . ' --password=' . $this->password . - ($this->querynspace != '' ? ' --namespace=' . $this->querynspace:'') . - ' //' . trim($this->hostname) . + ($this->querynspace != '' ? ' --namespace=' . $this->querynspace : '') . + ' //' . $this->hostname . ' ' . $this->command; } @@ -255,12 +255,27 @@ function exec() { } } + /* A hostname/namespace never legitimately contains shell or cmd.exe + * metacharacters. Strip them (cmd.exe ignores \" and toggles quoting on + * every ", and expands %VAR%) before quoting so a device-supplied address + * cannot inject a command on the Cacti server. */ + function wmi_clean_arg($value) { + global $config; + + if (isset($config['cacti_server_os']) && $config['cacti_server_os'] == 'win32') { + $value = str_replace(array('"', '&', '|', '^', '<', '>', '(', ')', '%'), '', $value); + } + + return $value; + } + function clean() { - $this->username = cacti_escapeshellarg($this->username); - $this->password = cacti_escapeshellarg($this->password); - $this->hostname = trim($this->hostname); - $this->binary = cacti_escapeshellarg($this->binary); - $this->command = cacti_escapeshellarg($this->command); + $this->username = cacti_escapeshellarg($this->username); + $this->password = cacti_escapeshellarg($this->password); + $this->hostname = cacti_escapeshellarg($this->wmi_clean_arg(trim($this->hostname))); + $this->querynspace = ($this->querynspace != '' ? cacti_escapeshellarg($this->wmi_clean_arg($this->querynspace)) : ''); + $this->binary = cacti_escapeshellarg($this->binary); + $this->command = cacti_escapeshellarg($this->command); } function retrieve_account() { diff --git a/tests/WmiCommandInjectionTest.php b/tests/WmiCommandInjectionTest.php new file mode 100644 index 0000000..4abdfd7 --- /dev/null +++ b/tests/WmiCommandInjectionTest.php @@ -0,0 +1,58 @@ + 'unix'); + +if (!function_exists('cacti_escapeshellarg')) { + function cacti_escapeshellarg($s) { return escapeshellarg($s); } +} + +require_once __DIR__ . '/../linux_wmi.php'; + +$fail = 0; + +function check($cond, $msg) { + global $fail; + if ($cond) { + print " ok: $msg\n"; + } else { + print " FAIL: $msg\n"; + $fail = 1; + } +} + +/* unix: an injected hostname must be single-quote contained, not break out */ +$w = new Linux_WMI(); +$w->username = 'u'; +$w->password = 'p'; +$w->binary = '/usr/bin/wmic'; +$w->command = 'SELECT Name FROM Win32_OperatingSystem'; +$w->hostname = '127.0.0.1; touch /tmp/pwned #'; +$w->querynspace = "root\\CIMV2'; id #"; + +$cmd = $w->getcommand(); + +check(strpos($cmd, '; touch /tmp/pwned') === false || strpos($cmd, "'127.0.0.1; touch /tmp/pwned #'") !== false, + 'injected hostname is contained inside a quoted argument'); +check(preg_match('#//\x27#', $cmd) === 1, 'the target host is quoted (//\'...\')'); +check(strpos($cmd, "--namespace='") !== false, 'namespace is quoted'); + +/* win32: metacharacters are stripped from the hostname before quoting */ +$GLOBALS['config']['cacti_server_os'] = 'win32'; +$w2 = new Linux_WMI(); +$w2->username = 'u'; $w2->password = 'p'; $w2->binary = 'wmic'; $w2->command = 'x'; +$w2->hostname = 'host" & calc.exe & %USERNAME%'; +$cmd2 = $w2->getcommand(); +foreach (array('"', '&', '(', ')', '%') as $meta) { + check(strpos(substr($cmd2, strpos($cmd2, '//')), $meta) === false, "win32: '$meta' stripped from the target host"); +} + +exit($fail); From 04775af6e89ee8e93f749f7265f8877d29a41cb7 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 30 Aug 2026 00:25:04 -0700 Subject: [PATCH 4/4] security: quote the wmic delimiter for the shell The default separator (|+|) contains pipe characters, so the unquoted --delimiter=|+| made exec() split the command into a shell pipeline (exit 127, no data). Quote it in getcommand() while keeping the property raw for the explode() in fetch(). issue#5 Signed-off-by: Thomas Vincent --- linux_wmi.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/linux_wmi.php b/linux_wmi.php index 17147ab..e246522 100644 --- a/linux_wmi.php +++ b/linux_wmi.php @@ -219,8 +219,11 @@ function getcommand() { $this->clean(); + // Quote the delimiter for the shell only. $this->separator stays raw for + // the explode() in fetch(); its default (|+|) is otherwise split as a + // shell pipeline. return $this->binary . - ' --delimiter=' . $this->separator . + ' --delimiter=' . cacti_escapeshellarg($this->separator) . ' --user=' . $this->username . ' --password=' . $this->password . ($this->querynspace != '' ? ' --namespace=' . $this->querynspace : '') .