Skip to content

feat(security): consolidated security hardening — forward-port from 1.2.x + DOM XSS + JS fixes#7055

Closed
somethingwithproof wants to merge 64 commits into
Cacti:developfrom
somethingwithproof:feat/security-consolidated-develop
Closed

feat(security): consolidated security hardening — forward-port from 1.2.x + DOM XSS + JS fixes#7055
somethingwithproof wants to merge 64 commits into
Cacti:developfrom
somethingwithproof:feat/security-consolidated-develop

Conversation

@somethingwithproof
Copy link
Copy Markdown
Contributor

Forward-port of #7054 (architectural security helpers) plus DOM XSS fixes and JS error corrections.

35 commits covering: cacti_exec, cacti_http_fetch, cacti_ldap_filter, cacti_safe_write, cacti_plugin_path, cacti_dispatch, cacti_auth_transition, typed request helpers, db_fetch_row guards, CSRF hardening, CSP/HSTS, DOM XSS escaping, JS null-deref fixes, SHA-256 enforcement, PHPStan CI.

Consolidates and supersedes: #7049, #7053.

Signed-off-by: Thomas Vincent thomasvincent@gmail.com

somethingwithproof and others added 30 commits April 16, 2026 21:19
Centralized shell command gateway routing all execution through
cacti_exec() with argv-array input, per-argument escaping, optional
timeout via proc_open + stream_select, and automatic credential
redaction in logs. Migrated structure_rra_paths.php to use bounded
GET_LOCK retry with exponential backoff and shutdown-function
release guard.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Add early SAPI !== cli rejection in cli_check.php returning 404.
Tighten Content-Security-Policy from default-src * to default-src
'self' with explicit per-directive sources, add HSTS on HTTPS, and
add COOP/CORP headers. Add XML size-limit checks and structured
parse-error logging in install/functions.php and lib/import.php.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Centralized outbound HTTP fetch with reserved-IP rejection via
FILTER_FLAG_NO_RES_RANGE, TLS peer verification by default,
redirect-following disabled, and configurable timeout. Prevents
SSRF by resolving hostnames and checking all A records against
RFC 1918/6598/loopback/link-local ranges before connecting.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Add cacti_ldap_filter() for safe LDAP filter construction using
ldap_escape() with LDAP_ESCAPE_FILTER per substitution variable.
Replaces raw string interpolation in search filter templates.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Add get_request_sort() for whitelist-only column/direction extraction,
get_request_ids() for integer-only ID array parsing, and db_qstr_like()
for safe SQL LIKE clause construction with percent/underscore escaping.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Add cacti_dispatch() declarative action table replacing ad-hoc
switch/case on \$_REQUEST['action']. Enforces HTTP method, realm
permission, and optional object-level ACL callback before dispatch.
Logs unknown actions and method mismatches.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Apply html_escape() to unescaped description fields in managers.php
SNMP notification tooltip and three locations in html_form.php where
field_array description was rendered as raw HTML in formFieldDescription
spans and display_tooltip() calls.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ntion

Add cacti_auth_transition() to lib/auth.php for safe privilege-level
changes. Regenerates session ID, checks lockout status, rotates
remember-me cookie token, and invalidates cached permission data.
Intended for login, password change, and role-switch call sites.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Document current csrf-magic state, known gaps, and short/medium/long
term migration plan toward explicit token embedding and SameSite
cookie enforcement.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Add phpstan.neon targeting new security gateway files at level 5 with
ignore rules for Cacti global functions not visible to PHPStan. Add
GitHub Actions workflow running PHPStan on PHP 8.1 for push/PR to
1.2.x branch.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Add Pest source-scanning tests verifying all 9 security architecture
items: exec gateway, HTTP SSRF protection, LDAP filter escaping,
request helpers, action dispatcher, XSS escaping, auth transition,
CSP/HSTS headers, and XML hardening.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Atomic write with realpath containment. Rejects absolute paths,
traversal segments, and symlinks that escape the allowed base
directories. Post-write verification via realpath. Eliminates the
file-write/path-traversal vulnerability class in package imports.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
basename() on plugin name, realpath containment check against the
plugins directory. Eliminates the LFI/include-path vulnerability
class across all 5 plugin include sites in lib/plugins.php.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
All 4 include_once sites in lib/plugins.php now use cacti_plugin_path()
which enforces basename + realpath containment. Rejects traversal
attempts and paths that resolve outside the plugins directory.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Force SHA-256 for package signature verification. The legacy SHA-1 key
path is commented out. Packages signed with the old key must be
re-signed with the SHA-256 key to import.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
When graph.php is loaded without a valid local_graph_id the existing
$exists check at line 66 can pass due to MySQL implicit type coercion
(WHERE local_graph_id = '' matches id 0). The subsequent db_fetch_row
at line 105 returns false, and line 114 dereferences
$graph['graph_template_id'] on a non-array, producing PHP 8.x
'Undefined array key' warnings for graph_template_id, rows, steps,
and id.

Add a cacti_sizeof guard after the graph fetch and redirect to
graph_view.php with raise_message() so the user sees a clear error
instead of a stack of PHP warnings.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Pest source-scan tests confirming the cacti_sizeof($graph) guard
exists after db_fetch_row_prepared, fires raise_message with redirect
to graph_view.php, and exits before any $graph['...'] dereference.

All syntax is PHP 7.4 compatible (no arrow functions, no match
expressions, no named arguments).

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ipts

Prevent null array dereferences when db_fetch_row_prepared returns
an empty result for a missing or deleted record.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ures

Per TheWitness: guard-failure redirects should return the user to where
they came from, not to a hardcoded page head. Use validate_redirect_url
with HTTP_REFERER and a safe fallback to the file's own list view.

Applies to all 16 guard sites added in 34cdca9 plus the graph.php
guard from ddc5c35.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The graph.php guard now redirects via validate_redirect_url with
HTTP_REFERER instead of a hardcoded Location header. Update the
test assertion to match.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Setting cache_directory to the Cacti install root itself passed the
strpos prefix check because equality was not rejected. Tighten to
require strict subdirectory (base + DIRECTORY_SEPARATOR prefix).

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
tempnam() files persist until removed. The import flow created a temp
file but never cleaned it up, leaking one file per import operation.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
index.php and script_server.php had identical normalization + prefix
blocks for dynamic-include confinement. Replace both with the new
cacti_path_is_within helper in lib/functions.php.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Source-scan tests for cacti_csv_safe (tab/CR in dangerous list, ltrim
applied), cacti_path_is_within usage in index.php and script_server.php,
db_replace redaction of snmp_auth_passphrase and rsa_private_key, and
boost cache-directory equality rejection.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ion/purge actions

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Csrf-magic fallback cookie now sets Secure, HttpOnly, SameSite=Strict.
Token comparisons use hash_equals() instead of raw ===. GET logout
documented as risk-accepted (annoyance-only, SameSite mitigates).
Test coverage for all three changes.

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>
… null check

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…, hi-IN, it-IT, ja-JP, ko-KR, lv-LV, pl-PL, ru-RU, uk-UA

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…acti_validate_sort_column

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…lidation.php, remove duplicate helper

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
… duplicate

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ggregate and reports

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
… failures

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…xport, query_host_cpu

Local branch diverged from upstream before PR Cacti#6941 was merged. Restore the
scripts from upstream/develop so DatasourceScriptErrorReturnTest passes.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…acti#7057)

Consolidated PR now only contains its unique work, avoiding merge conflicts
with the companion PRs. Files moved to their respective PRs:
- lib/ping.php → Cacti#7057
- theme/nav/layout files → Cacti#7040
- test files, workflow files, security helpers → Cacti#7036

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This branch contains only files unique to the consolidated security
work that are not already covered by:
- Cacti#7036 (security hardening batch)
- Cacti#7040 (tooltip pin / theme+nav changes)
- Cacti#7057 (ping.php alignment)

Previously shipped as feat/security-consolidated-develop with 124 files;
rebased to eliminate merge conflicts by removing duplicated content.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@somethingwithproof somethingwithproof force-pushed the feat/security-consolidated-develop branch 2 times, most recently from 8bd240e to 7523bc2 Compare April 22, 2026 18:59
@TheWitness
Copy link
Copy Markdown
Member

@somethingwithproof, This pull unwinds a lot of changes in the develop branch. It's going to take a lot of work to unwind the unwinding.

@somethingwithproof
Copy link
Copy Markdown
Contributor Author

OK, I will take a look

Add .claude/, .omc/, .worktrees/, and notepad.md to .gitignore so local
AI-assistant session state, oh-my-claudecode orchestration files, and
scratch worktrees never leak into a PR. Add CLAUDE.md at the repo root
documenting the house conventions (PHP 7.4 target on 1.2.x branches,
cacti_sizeof/htmle/gfrv wrappers, db_qstr_rlike, the vendor-pollution
pitfall) so future AI contributions match the review feedback pattern
from TheWitness instead of repeating it on every PR.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@somethingwithproof
Copy link
Copy Markdown
Contributor Author

Added .claude//.omc//.worktrees/ to .gitignore and a CLAUDE.md at the repo root to prevent the pollution that's been leaking in. On the unwinding concern — agreed, the scope is too wide. I'll either split this into smaller per-area PRs (XSS, CSRF, input validation, etc.) or close it in favor of targeted backports. Which would you prefer?

5 local oh-my-claudecode artefacts (.omc/sessions/*.json and
.omc/state/*) were staged into this branch at some point and persisted
across rebases. The .gitignore entry added in the previous commit
stops new ones from being added, but the already-tracked files need a
separate git rm --cached to leave the index.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
@somethingwithproof
Copy link
Copy Markdown
Contributor Author

Closing per your feedback that this unwinds too many already-landed develop changes. The audit also flagged that the auth_profile.php hunks in this branch reintroduce the CVE-2026-39900 XSS and open-redirect that were just fixed on develop, which is the opposite of what this PR should do.

The one genuinely-unique helper (the declarative action dispatcher) has been extracted to a tight single-file PR at #7063. The existing helper files that overlap with #7054 will land there as part of the 1.2.x architectural branch; forward-porting them to develop can happen on a clean rebase once #7054 settles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants