-
Notifications
You must be signed in to change notification settings - Fork 31
hardening: prepared statements, PHP 7.4 idioms, and security fixes #215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
824110b
324a740
e574bf6
9cea833
69d4188
10420ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "name": "cacti/plugin_weathermap", | ||
| "description": "plugin_weathermap 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" | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -100,7 +100,15 @@ 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"; | ||||||||||||||||||
| /* Validate before exec: only IP addresses and hostnames are allowed. | ||||||||||||||||||
| * Shell metacharacters in $target would otherwise reach popen() directly. */ | ||||||||||||||||||
| if (!preg_match('/^[a-zA-Z0-9.\-:]+$/', $target)) { | ||||||||||||||||||
|
Comment on lines
+103
to
+105
|
||||||||||||||||||
| /* Validate before exec: only IP addresses and hostnames are allowed. | |
| * Shell metacharacters in $target would otherwise reach popen() directly. */ | |
| if (!preg_match('/^[a-zA-Z0-9.\-:]+$/', $target)) { | |
| /* Validate before exec, but keep compatibility with fping targets that are | |
| * valid in practice (for example IPv6 zone identifiers like fe80::1%eth0). | |
| * escapeshellarg() is the shell-safety boundary here, so only reject empty | |
| * values and ASCII control characters. */ | |
| if ($target === '' || preg_match('/[\x00-\x1F\x7F]/', $target)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid. The regex is too restrictive for IPv6 and paths with underscores. Will expand the character class.
Copilot
AI
Apr 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even after adding shell escaping, $target is still interpolated into the $pattern regex earlier in this method. Because . is allowed and has special meaning in regex, a hostname/IP containing dots can match unintended lines (e.g., example.com would also match exampleXcom). Consider building the pattern using preg_quote($target, '/') (after validation) so it matches the literal target.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid. Will use escapeshellarg on the path components.
Copilot
AI
Apr 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rejection warning logs the raw $target value. Because this branch is reached specifically for “illegal characters”, an attacker could include newlines/control characters and cause log injection/formatting issues. Consider sanitizing the value before logging (e.g., replace non-printables or log an encoded representation) while still preserving troubleshooting value.
| wm_warn("FPing ReadData: rejected target with illegal characters ($target) [WMFPING04]"); | |
| $log_target = json_encode($target, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); | |
| if ($log_target === false) { | |
| $log_target = '"[unencodable target]"'; | |
| } | |
| wm_warn("FPing ReadData: rejected target with illegal characters ($log_target) [WMFPING04]"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid. Will sanitize the logged value.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?php | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| */ | ||
|
|
||
| require_once __DIR__ . '/bootstrap.php'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| <?php | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| */ | ||
|
|
||
| describe('auth guard presence in weathermap', function () { | ||
| it('includes auth.php or global.php in all UI entry points', function () { | ||
| $uiFiles = array( | ||
| 'cli/cacti-mapper.php', | ||
| 'lib/WeatherMap.class.php', | ||
| 'lib/WeatherMap.functions.php', | ||
| 'lib/datasources/WeatherMapDataSource_fping.php', | ||
| 'lib/datasources/WeatherMapDataSource_rrd.php', | ||
| 'lib/editor.inc.php', | ||
| ); | ||
|
|
||
| foreach ($uiFiles as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
| if ($path === false) continue; | ||
| $contents = file_get_contents($path); | ||
| if ($contents === false) continue; | ||
|
|
||
| // Files that include setup.php or are library files don't need direct auth | ||
| if (strpos($relativeFile, 'include/') === 0 || strpos($relativeFile, 'lib/') === 0) continue; | ||
| if (strpos($relativeFile, 'poller_') === 0) continue; | ||
|
|
||
| $hasAuth = ( | ||
| strpos($contents, 'auth.php') !== false || | ||
| strpos($contents, 'global.php') !== false || | ||
| strpos($contents, 'global_arrays.php') !== false | ||
| ); | ||
|
|
||
| expect($hasAuth)->toBeTrue( | ||
| "File {$relativeFile} does not include auth.php or global.php" | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it('validates numeric IDs from request variables before DB queries', function () { | ||
| $uiFiles = array( | ||
| 'cli/cacti-mapper.php', | ||
| 'lib/WeatherMap.class.php', | ||
| 'lib/WeatherMap.functions.php', | ||
| 'lib/datasources/WeatherMapDataSource_fping.php', | ||
| 'lib/datasources/WeatherMapDataSource_rrd.php', | ||
| 'lib/editor.inc.php', | ||
| ); | ||
|
|
||
| foreach ($uiFiles as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
| if ($path === false) continue; | ||
| $contents = file_get_contents($path); | ||
| if ($contents === false) continue; | ||
|
|
||
| // Check for get_filter_request_var usage for numeric IDs | ||
| if (preg_match('/get_request_var\s*\(\s*[\'\"]id[\'\"]/', $contents)) { | ||
| // Should use get_filter_request_var for 'id' params | ||
| $hasFilter = ( | ||
| strpos($contents, 'get_filter_request_var') !== false || | ||
| strpos($contents, 'input_validate_input_number') !== false || | ||
| strpos($contents, 'form_input_validate') !== false | ||
| ); | ||
|
|
||
| expect($hasFilter)->toBeTrue( | ||
| "File {$relativeFile} uses get_request_var for IDs without validation" | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| <?php | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| */ | ||
|
|
||
| describe('output escaping in weathermap', function () { | ||
| it('does not interpolate raw variables into HTML attributes', function () { | ||
| $uiFiles = array( | ||
| 'cli/cacti-mapper.php', | ||
| 'lib/WeatherMap.class.php', | ||
| 'lib/WeatherMap.functions.php', | ||
| 'lib/datasources/WeatherMapDataSource_fping.php', | ||
| 'lib/datasources/WeatherMapDataSource_rrd.php', | ||
| 'lib/editor.inc.php', | ||
| ); | ||
|
|
||
| foreach ($uiFiles as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
| if ($path === false) continue; | ||
| $contents = file_get_contents($path); | ||
| if ($contents === false) continue; | ||
|
|
||
| $lines = explode("\n", $contents); | ||
| $dangerous = 0; | ||
|
|
||
| foreach ($lines as $line) { | ||
| $trimmed = ltrim($line); | ||
| if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0) continue; | ||
|
|
||
| // value="$row[...] without html_escape wrapping | ||
| if (preg_match('/value\s*=\s*["\'"]\s*<\?php\s+echo\s+\$/', $line)) { | ||
| $dangerous++; | ||
| } | ||
| // title="<?php print $something without escaping | ||
| if (preg_match('/(?:title|alt|placeholder)\s*=.*print\s+\$(?!_|config)/', $line)) { | ||
| if (strpos($line, 'html_escape') === false && strpos($line, '__esc') === false && strpos($line, 'htmlspecialchars') === false) { | ||
| $dangerous++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| expect($dangerous)->toBe(0, | ||
| "File {$relativeFile} has unescaped variables in HTML attributes" | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it('uses html_escape or __esc for user-controlled output', function () { | ||
| $uiFiles = array( | ||
| 'cli/cacti-mapper.php', | ||
| 'lib/WeatherMap.class.php', | ||
| 'lib/WeatherMap.functions.php', | ||
| 'lib/datasources/WeatherMapDataSource_fping.php', | ||
| 'lib/datasources/WeatherMapDataSource_rrd.php', | ||
| 'lib/editor.inc.php', | ||
| ); | ||
|
|
||
| $totalEscapeCalls = 0; | ||
|
|
||
| foreach ($uiFiles as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
| if ($path === false) continue; | ||
| $contents = file_get_contents($path); | ||
| if ($contents === false) continue; | ||
|
|
||
| $totalEscapeCalls += preg_match_all('/html_escape|__esc\(|htmlspecialchars/', $contents); | ||
| } | ||
|
|
||
| // At least some escaping should be present in UI files | ||
| expect($totalEscapeCalls)->toBeGreaterThan(0, | ||
| 'UI files should contain at least one html_escape/__esc call' | ||
| ); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.