feat(security): add cacti_* validation helper module (lib/security_validation.php)#7068
Conversation
…(batch 2) (#1) * fix: correct grammar and possessive errors in English source strings (batch 2) Seven errors found via cacti.pot audit: - automation_networks.php: "senders name" → "sender\'s name" (line 839) - automation_networks.php: "senders Email" → "sender\'s Email" (line 848) - automation_templates.php: "devices sysDescr" → "device\'s sysDescr" (line 1060) - automation_templates.php: "devices sysName" → "device\'s sysName" (line 1067) - automation_templates.php: "devices sysOid" → "device\'s sysOid" (line 1074) - data_templates.php: garbled "it either it value...critical data Data Query" → "its value...Data Query" (line 903) - package_repos.php: "Package Repositorites" → "Package Repositories" (line 436) All changes are to UI description strings inside __() / __esc() calls. cacti.pot should be regenerated via build_gettext.sh after merge. * fix: correct grammar and possessive errors in English source strings (batch 2) Seven errors found via cacti.pot audit: - automation_networks.php: "senders name" → "sender\'s name" (line 839) - automation_networks.php: "senders Email" → "sender\'s Email" (line 848) - automation_templates.php: "devices sysDescr" → "device\'s sysDescr" (line 1060) - automation_templates.php: "devices sysName" → "device\'s sysName" (line 1067) - automation_templates.php: "devices sysOid" → "device\'s sysOid" (line 1074) - data_templates.php: garbled string → "its value...Data Query" (line 903) - package_repos.php: "Package Repositorites" → "Package Repositories" (line 436) All changes are to UI description strings inside __() / __esc() calls. cacti.pot should be regenerated via build_gettext.sh after merge. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Improve IPv6 support in RRDtool proxy and ping utilities
- Fix RRDtool proxy to dynamically detect IPv6 addresses and use
AF_INET6 sockets instead of hardcoded AF_INET (IPv4-only), enabling
connections to IPv6 RRDtool proxy servers including backup servers
- Strip brackets from IPv6 addresses before passing to socket_connect
- Properly close failed sockets before creating new ones for failover
- Replace fragile str_contains(':') IPv6 detection in ping with
filter_var(FILTER_FLAG_IPV6) for more robust address validation
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Fix JS code quality: implicit globals, deprecated APIs, loose equality
- Add var declarations to prevent implicit globals in install.js
(element, enabled, button, buttonCheck)
- Remove console.log debug output left in production (install.js)
- Replace deprecated jQuery .unbind() with .off() (layout.js)
- Fix "depreciated" typo to "deprecated" in deprecation warnings
- Convert == / != to === / !== for null, boolean, string, typeof,
and numeric comparisons across install.js and realtime.js
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Replace deprecated jQuery shorthand event methods in layout.js
Replace .click(), .keyup(), .keydown(), .mousedown(), .mouseenter(),
.mouseleave(), .submit(), .resize() with .on() equivalents. Replace
.focus(), .change() trigger calls with .trigger(). These shorthands
were deprecated in jQuery 3.5.
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Replace deprecated jQuery methods in realtime.js
Replace .bind() with .on() and .change() trigger calls with
.trigger('change'). .bind() was deprecated in jQuery 3.0 and
shorthand triggers in jQuery 3.5.
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Replace deprecated jQuery methods in install.js and fix ===/!== errors
Replace .click(), .change(), .focus() with .on()/.trigger() equivalents.
Also fix !=== and ==== operators that were incorrectly introduced by a
prior replace-all of == to === within existing !== and === expressions.
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Restore == null where needed to catch both null and undefined
In JavaScript, == null matches both null and undefined, which is an
intentional idiom. The prior === null conversion broke cases where
values come from jQuery .val(), .data(), $.urlParam(), or object
property access that may return undefined rather than null. Revert
those specific cases while keeping === null where variables are
explicitly initialized to null.
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Replace deprecated jQuery methods in all theme main.js files
Replace .unbind().click() with .off('click').on('click'), convert
.hover() to .on('mouseenter').on('mouseleave'), replace .change(),
.scroll(), .click() shorthands with .on() equivalents, and .blur()
with .trigger('blur') across all 10 theme files.
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Convert string concatenation to template literals
Replace 'str' + var + 'str' patterns with ES6 template literals
in realtime.js and install.js. Improves readability especially for
URL construction and HTML building. Also replace $.parseJSON() with
native JSON.parse() in realtime.js.
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
* Convert var to const for single-assignment variables
Replace var with const where the variable is assigned once and never
reassigned within its scope, in install.js and realtime.js. Keeps var
for variables that are conditionally reassigned (e.g. size, url).
https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo
---------
Co-authored-by: Claude <noreply@anthropic.com>
Six pure validation helpers in a new lib/security_validation.php module. Three generic (input-string safety, sort-column allowlist, sort-direction allowlist), one collection helper (positive-int ID filter), two remote-agent host-auth helpers used by the poller ingress. All side-effect free and individually unit tested. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new reusable security validation helper module for Cacti (PHP) with accompanying unit tests, and also includes several unrelated UX/JS event-binding refactors plus some IPv6-related networking adjustments.
Changes:
- Add
lib/security_validation.phpwith pure helper functions for input-string safety checks, sort validation, id extraction, and remote-agent host checks. - Add Pest unit tests covering the new validation helpers.
- Refactor various JavaScript event bindings (
unbind/click/hover→off/on) and make minor wording/typo fixes; adjust RRDtool proxy socket initialization for IPv6.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
lib/security_validation.php |
Adds new security/validation helper functions intended for reuse by callers. |
tests/Unit/SecurityValidationHelpersTest.php |
Adds Pest unit tests for the new helper module. |
lib/rrd.php |
Updates RRDtool proxy socket creation/connection flow to better support IPv6. |
lib/ping.php |
Switches IPv6 detection to filter_var(..., FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) in multiple ping paths. |
include/realtime.js |
Refactors string building to template literals and tightens comparisons / event triggering in realtime graph code. |
include/layout.js |
Refactors several event bindings to on(...)/off(...) and fixes “deprecated” spelling in console logs. |
install/install.js |
Refactors event bindings and string building to template literals; minor null/undefined handling tweaks. |
include/themes/sunrise/main.js |
Replaces unbind()/hover()/click() patterns with off()/on() equivalents. |
include/themes/raspberry/main.js |
Replaces unbind()/hover()/click() patterns with off()/on() equivalents. |
include/themes/paw/main.js |
Replaces unbind()/hover()/click() patterns with off()/on() equivalents. |
include/themes/paper-plane/main.js |
Replaces unbind()/hover()/click() patterns with off()/on() equivalents. |
include/themes/modern/main.js |
Replaces change() handlers with on('change', ...) equivalents. |
include/themes/midwinter/main.js |
Replaces click()/hover() bindings with on(...) equivalents; minor trigger adjustments. |
include/themes/hollyberry/main.js |
Replaces unbind()/hover()/click() patterns with off()/on() equivalents. |
include/themes/dark/main.js |
Replaces change()/hover() bindings with on(...) equivalents. |
include/themes/carrot/main.js |
Replaces unbind()/hover()/click() patterns with off()/on() equivalents. |
include/themes/cacti/main.js |
Replaces unbind()/hover()/click() patterns with off()/on() equivalents. |
package_repos.php |
Fixes UI spelling (“Repositories”). |
data_templates.php |
Improves wording/grammar in a UI message string. |
automation_templates.php |
Fixes grammar in template description strings (“device’s …”). |
automation_networks.php |
Fixes grammar/possessives in notification description strings (“sender’s …”). |
.github/workflows/codeql.yml |
Adds a CodeQL workflow (JS/TS + Python matrix). |
.github/dependabot.yml |
Adds Dependabot configuration for npm and GitHub Actions updates. |
Comments suppressed due to low confidence (1)
include/realtime.js:184
- In
stopRealtime(), the selector passed tozoom()is missing the#prefix (currentlygraph_${graph_id}). This will not match the intendedid="graph_<id>"element and will prevent zoom from being re-initialized when realtime stops. Use an id selector consistent with the rest of the file (e.g., select#graph_${graph_id}).
$(`graph_${graph_id}`).zoom({
inputfieldStartTime : 'date1',
inputfieldEndTime : 'date2',
serverTimeOffset : timeOffset
});
| return array_values(array_filter(array_map('intval', $items), function ($v) { | ||
| return $v > 0; | ||
| })); |
There was a problem hiding this comment.
cacti_extract_positive_int_ids() is documented as returning “strictly positive integers”, but using intval will accept strings like "9 OR 1=1" as 9 (and "4.2" as 4). If this is meant for sanitizing request id lists, it should reject non-integer tokens instead of truncating them. Consider filtering by a full-match integer regex (optionally allowing surrounding whitespace / leading +) before casting.
| return array_values(array_filter(array_map('intval', $items), function ($v) { | |
| return $v > 0; | |
| })); | |
| $ids = []; | |
| foreach ($items as $item) { | |
| if (is_int($item)) { | |
| $value = $item; | |
| } elseif (is_string($item)) { | |
| $item = trim($item); | |
| if (!preg_match('/^\+?[0-9]+$/', $item)) { | |
| continue; | |
| } | |
| $value = (int) $item; | |
| } else { | |
| continue; | |
| } | |
| if ($value > 0) { | |
| $ids[] = $value; | |
| } | |
| } | |
| return array_values($ids); |
| test('security helpers: remote agent forward lookup requires exact ip match', function () { | ||
| $allowed = cacti_remote_agent_forward_matches('10.0.0.5', 'poller.example.com', function () { | ||
| return [ | ||
| ['ip' => '10.0.0.4'], | ||
| ['ipv6' => '2001:db8::42'], | ||
| ['ip' => '10.0.0.5'], | ||
| ]; | ||
| }); |
There was a problem hiding this comment.
The $dns_lookup test double is declared with no parameters, but cacti_remote_agent_forward_matches() calls it with $client_name. In newer PHP versions this can emit warnings (or errors depending on configuration). Update the closures to accept the host parameter to match the documented callable signature.
| $blocked = cacti_remote_agent_forward_matches('10.0.0.7', 'poller.example.com', function () { | ||
| return [ | ||
| ['ip' => '10.0.0.4'], | ||
| ['ipv6' => '2001:db8::42'], | ||
| ]; | ||
| }); |
There was a problem hiding this comment.
Same as above: this $dns_lookup closure should accept the host parameter since cacti_remote_agent_forward_matches() passes one argument.
| test('security helpers: managers id extraction keeps only positive integers', function () { | ||
| $ids = cacti_extract_positive_int_ids(['1', '-5', '0', '4.2', 'abc', '7', '9 OR 1=1', ' +7', ' 08 ']); | ||
|
|
||
| expect($ids)->toBe([1, 4, 7, 9, 7, 8]); |
There was a problem hiding this comment.
This test asserts that cacti_extract_positive_int_ids() keeps values like "9 OR 1=1" by truncating them to 9. That contradicts the function’s “strictly positive integers” contract and encourages unsafe usage. If the helper is meant for sanitizing request ids, update the expected output to drop non-integer tokens and adjust the helper accordingly.
| expect($ids)->toBe([1, 4, 7, 9, 7, 8]); | |
| expect($ids)->toBe([1, 7, 8]); |
| // Close the failed socket before creating a new one for the backup server | ||
| @socket_close($rrdp_socket); | ||
|
|
||
| $rrdp_id = ($rrdp_id + 1) % 2; |
There was a problem hiding this comment.
In the load-balancing fallback, $rrdp_id = ($rrdp_id + 1) % 2; produces 0 or 1, while elsewhere $rrdp_id is treated like 1/2 (e.g., for logging and selecting primary vs backup config). Use a toggle that preserves the 1/2 convention (e.g., 3 - $rrdp_id) to avoid confusing log output and potential future logic errors.
| $rrdp_id = ($rrdp_id + 1) % 2; | |
| $rrdp_id = 3 - $rrdp_id; |
| // Detect address family based on server address for IPv6 support | ||
| $socket_family = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET; | ||
|
|
||
| $rrdp_socket = @socket_create($socket_family, SOCK_STREAM, SOL_TCP); | ||
|
|
||
| if ($rrdp_socket === false) { | ||
| cacti_log('CACTI2RRDP ERROR: Unable to create socket to connect to RRDtool Proxy Server', false, $logopt, POLLER_VERBOSITY_LOW); | ||
|
|
||
| $rrdp = socket_connect($rrdp_socket, $server, $port); | ||
| return false; | ||
| } | ||
|
|
||
| // Strip brackets from IPv6 addresses for socket_connect | ||
| $connect_server = trim($server, '[]'); | ||
|
|
||
| $rrdp = socket_connect($rrdp_socket, $connect_server, $port); |
There was a problem hiding this comment.
PR description focuses on adding lib/security_validation.php + tests and says no callers are migrated, but this PR also includes functional changes in unrelated areas (e.g., RRDtool proxy socket handling/IPv6 logic here, ping IPv6 detection, and multiple UI JS event handler refactors). If these are intentional, please reflect them in the PR description or split them into separate PRs to keep the security-helper change easy to review and backport.
| * Allows dotted columns (h.hostname), backtick-quoted identifiers, | ||
| * aggregate functions (COUNT(*), FIELD(status,1,3,2)), and spaces | ||
| * inside function calls. Blocks semicolons, comments, and UNION. | ||
| */ | ||
| function cacti_validate_sort_column(string $value, string $default = ''): string { | ||
| if (preg_match('/;|--|\/\*|\bUNION\b|\bDROP\b|\bDELETE\b|\bINSERT\b|\bUPDATE\b/i', $value)) { | ||
| return $default; | ||
| } | ||
|
|
||
| return preg_match('/^[a-zA-Z_`][a-zA-Z0-9_`()., *]*$/', $value) ? $value : $default; |
There was a problem hiding this comment.
cacti_validate_sort_column() is still permissive enough to allow arbitrary SQL expressions/functions (e.g., SLEEP(5), RAND()) because parentheses, commas, spaces, and * are allowed and only a few keywords are blocked. If callers use this to validate user-provided ORDER BY, it can still enable time-based attacks or expensive sorts. Consider restricting to identifiers / dotted identifiers (and, if needed, explicitly allow only specific safe functions like FIELD(...) via dedicated patterns) rather than a broad character allowlist.
| * Allows dotted columns (h.hostname), backtick-quoted identifiers, | |
| * aggregate functions (COUNT(*), FIELD(status,1,3,2)), and spaces | |
| * inside function calls. Blocks semicolons, comments, and UNION. | |
| */ | |
| function cacti_validate_sort_column(string $value, string $default = ''): string { | |
| if (preg_match('/;|--|\/\*|\bUNION\b|\bDROP\b|\bDELETE\b|\bINSERT\b|\bUPDATE\b/i', $value)) { | |
| return $default; | |
| } | |
| return preg_match('/^[a-zA-Z_`][a-zA-Z0-9_`()., *]*$/', $value) ? $value : $default; | |
| * Allows bare identifiers, dotted identifiers (h.hostname), and | |
| * backtick-quoted identifiers. Does not allow SQL expressions or | |
| * function calls. | |
| */ | |
| function cacti_validate_sort_column(string $value, string $default = ''): string { | |
| $value = trim($value); | |
| if ($value === '') { | |
| return $default; | |
| } | |
| return preg_match('/^(?:`[a-zA-Z_][a-zA-Z0-9_]*`|[a-zA-Z_][a-zA-Z0-9_]*)(?:\.(?:`[a-zA-Z_][a-zA-Z0-9_]*`|[a-zA-Z_][a-zA-Z0-9_]*))*$/', $value) ? $value : $default; |
| function cacti_remote_agent_is_authorized_host(string $client_name, string $client_addr, array $pollers, array $whitelist): bool { | ||
| foreach ($pollers as $poller) { | ||
| if (isset($poller['hostname']) && (strcasecmp($poller['hostname'], $client_name) === 0 || $poller['hostname'] === $client_addr)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return in_array($client_addr, $whitelist, true); |
There was a problem hiding this comment.
PR description says cacti_remote_agent_is_authorized_host() “combines the above” (including forward-DNS matching), but the current implementation only checks the poller hostname / client IP against lists and never calls cacti_remote_agent_forward_matches(). Either update the implementation to incorporate forward-lookup matching (likely via an injected $dns_lookup callable for testability) or adjust the PR description/API to reflect the actual behavior.
|
Closing. Copilot flagged two substantive bugs plus the fork-stale-files issue: Function-contract bugs
Fork-stale contamination My fork's No salvageable path that isn't "rewrite the helpers with stricter contracts and re-file against upstream." Closing. |
Adds a side-effect-free validation helper module with six pure functions callers can use instead of open-coding their own regex allowlists.
Exported
cacti_input_string_is_safe(string): bool— data-input command-string safety. Strips<tag>placeholders, then blocks;,`,$, and newlines.cacti_validate_sort_column(string, string = ''): string— sort-column allowlist forORDER BY. Blocks;,--,/*,UNION/DROP/DELETE/INSERT/UPDATE; requires[a-zA-Z_``]prefix; allows dotted columns, backticks, parentheses, spaces forFIELD(status,1,3,2)style.cacti_validate_sort_direction(string, string = 'ASC'): string—ASC|DESCallowlist.cacti_extract_positive_int_ids(array): array— filter-casts an array to strictly positive ints, drops everything else.cacti_remote_agent_forward_matches(string, string, callable): bool— forward-DNS match helper for poller ingress.cacti_remote_agent_is_authorized_host(string, string, array, array): bool— ingress authorization combining the above with a poller list + whitelist.Tests
tests/Unit/SecurityValidationHelpersTest.php(80 lines, Pest) covers each helper's positive case, negative case, and at least one edge case.Scope
Re-file from closed batch PR #7036.