mirror of
https://github.com/Cacti/cacti.git
synced 2026-11-03 10:37:46 +09:00
security-batch-12
1164
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f58a4a9eb |
merge: resolve security batch conflicts with develop
Preserve package and inline JavaScript coverage safeguards. Retain SpikeKill last-value repair with boundary and zero samples, and the updated PHP action pin. Apply required formatting to incoming test fixtures. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
b95226918e |
fix: Update the deepness favicon (#8419)
* fix: Update the deepness favicon (cherry picked from commit |
||
|
|
433af99661 |
Fix plugin archive restore skipping not-yet-created nested directories (#8398)
Restoring a plugin from an archive, or installing one downloaded from GitHub into the database, extracts the tgz to a temp file and writes each entry under plugins/<name>/. validate_relative_path_within() anchored a not-yet-created entry on its immediate parent directory, so every entry in a subdirectory the restore had not created yet (includes/, css/, tests/, docs/, ...) resolved against a missing path, failed the containment check and was skipped. The plugin was left with only its top-level files and setup.php then fatally required the missing includes/database.php. Judge a not-yet-created entry by its deepest existing ancestor instead, mirroring data_source_path_within_rra(). Every path segment is already a validated plain name and no walked segment may be a symlink, so the full candidate still stays within the base; traversal, absolute, drive-letter, null-byte and symlink entries remain rejected. |
||
|
|
cf70ba4cf2 |
fix(plugins): [develop] restore lifecycle diagnostics, compatibility and error attribution (#8297)
* fix(plugins): honor version range boundaries and compatibility status Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): preserve null hook diagnostics and enforce dependency versions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): report config failures and confine error attribution Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): guard entrypoint frames and bind realm filenames Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): propagate install hook failures on develop Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(plugins): match realm filenames literally Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): defer release entries to final backlog PR Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(plugins): add standard GPL headers Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci(coverage): require 80 percent of changed PHP statements Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: share changed-code coverage contracts and enforce shell policy Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(ci): run request coverage with portable path matching Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: retain bootstrap coverage and validate request dispatch Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: synchronize native coverage fixture dispatch Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: measure coverage at the exact PR revision Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
ebd16105c5 |
fix: drop backslash and line breaks in sanitize_uri (#8388)
* fix: drop backslash and line breaks in sanitize_uri Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: add changelog entry for sanitize_uri fix Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
0f06608175 |
fix: reject dangling symlinks in validate_relative_path_within (#8389)
* fix: reject dangling symlinks in validate_relative_path_within Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: add changelog entry for dangling symlink fix Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
1aba203d3e |
fix(security): reject ambiguous shell expansion contexts
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
06f49d824a |
fix(security): reject script fields in backtick substitutions
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
3ade3032ee |
Merge remote-tracking branch 'origin/develop' into security-batch-12
# Conflicts: # .github/workflows/pr-changed-coverage.yml |
||
|
|
930b085703 |
feat(auth): log administrative account actions after confirmed writes (#8360)
* log user action (create, delete, locked, ..) * change log * update changelog * style(auth): apply coding standards to user action logging Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Audit password changes only after successful account saves Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(auth): sanitize audit fields and confirm account removals Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: cover native account audit state transitions Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: synchronize native coverage fixture dispatch Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(auth): audit only confirmed account persistence Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: measure coverage at the exact PR revision Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: Petr Macek <petr.macek@kostax.cz> |
||
|
|
3c9678007a |
fix(security): reject unsupported nested shell grammar in #8237 (#8359)
* fix(security): reject unsupported nested shell grammar Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(security): exercise nested grammar rejection in CI coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
d822f37a06 | style: short array syntax and php-cs-fixer formatting in substitute_script_path | ||
|
|
eb4ca6b53e |
fix(security): clear quote-empty flag on single-quote content; cover new parser branches
The stack-based substitute_script_path() rewrite left quoteEmpty set while emitting literal content inside a single-quoted region, so a template like '\<field>' wrongly took the exact-wrap branch and dropped the backslash (ScriptSubstitutionMatrix 'shell 56'). Clear the frame's quoteEmpty whenever a single-quoted character is emitted, matching the pre-rewrite fallthrough. ScriptSubstitutionMatrix.php: add 8 explicit parser cases exercising the \/backtick nested contexts and escaped \$ / backtick pairing so every rewritten line is covered by the changed-coverage probe (1285 -> 1293). |
||
|
|
688cc1bf35 |
Merge remote-tracking branch 'origin/develop' into security-batch-12
# Conflicts: # .github/workflows/pr-changed-coverage.yml |
||
|
|
abd0acae45 |
security: address review - nested cmd-sub/backtick handling, coverage gate, test skip
substitute_script_path() now follows the template's own $(...) and backtick
command substitutions as nested parsing contexts with their own quote state, so a
token placed inside one ("$(printf %s <field>)") is judged against that fresh
context instead of a suspended outer double quote. This closes the
nested-reinterpretation bypass where an escaped value sitting inside a command
substitution could be re-activated by an outer double quote. Backslash pairing in
both the unquoted and double-quoted contexts now also neutralizes $ and backtick,
so an escaped command substitution (\$(...) or \`...`) is not mis-detected as live.
.github/workflows/pr-changed-coverage.yml: drop the hard-coded PR-number
allowlist so the job runs whenever the path filter selects it, rather than
silently skipping every unlisted PR once these merge.
tests/Unit/Security/ShellInjection/ScriptFieldShellRoundTripTest.php: skip when
either exec() or proc_open() is unavailable (the test launches the fixture with
exec() while the fixture itself uses proc_open()). The fixture adds 7 nested
command-substitution and backtick round-trip cases (90 -> 97), each proving the
field value echoes back verbatim with nothing inside it executing.
|
||
|
|
34fc3f8642 |
Stop flagging a not-yet-created RRA subdirectory as a data_source_path escape (#8313)
* Stop data_source_path_within_rra() flagging a not-yet-created intermediate directory as an RRA escape get_data_source_path() routed a legitimate <path_rra>/<host_id>/<local_data_id>.rrd through the generated-path fallback and logged a bogus 'SECURITY: ... escapes the RRA directory' message whenever the host_id subdirectory did not exist yet (first poll, or host_id 0). data_source_path_within_rra() only tolerated a missing final RRD file: for a missing file it resolved realpath(dirname(path)), which returns false when that parent directory has not been created, so the lexically-contained path was wrongly rejected. Walk to the deepest ancestor that actually exists and confirm it resolves inside the RRA root instead. Lexical containment (no .., '.', or empty segments) and per-segment symlink rejection are unchanged, so traversal, lookalike-prefix, absolute, and symlink-pivot paths are still rejected. Adds a regression test for a not-yet-created intermediate directory. * Drop explanatory comments from the RRA containment fix * Add CHANGELOG fragment for the RRA containment fix * Reject a dangling-symlink segment independently of file_exists() * test(paths): cover RRA filesystem boundaries and gate PR coverage Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: measure committed PR changes on a policy-compliant runner Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
c077d88916 |
fix(security): preserve literal backslashes in quoted script fields
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
ff31a77d8f |
security: normalize dangling backslash before substituted value in substitute_script_path (GHSA-5v3j)
A template backslash immediately before a <field> token (\<arg1>) escaped the already-escaped value's opening quote, producing \'...' which the shell rejects with an unmatched-quote error (odd backslash runs) or prepended a stray literal backslash (even runs). The quote-aware scanner now sheds one backslash when the run immediately preceding a self-quoted value is odd, in both the unquoted in-place branch and the close-around branch, so the value's own quotes always function as the shell's quotes and $(...)/backticks inside it can never re-activate. The backslash regression test now executes each substituted template through /bin/sh (printf %s) and asserts a clean parse plus a literal round-trip across odd/even backslash counts, rather than pinning the generated string. |
||
|
|
470e0c3803 |
security: keep substituting a token that follows a backslash in substitute_script_path
Mirror the 1.2.x fix (#8238): the scanner's backslash branches consumed the next character unconditionally, so a legacy/custom template token such as \<arg> never reached the matcher and the trailing cleanup stripped it to a bare backslash. Both branches now only pair the backslash with a following quote/backslash (so an escaped quote still cannot toggle quote-state); any other character - notably a <token> start - leaves the backslash on its own and is processed on the next iteration. Reformatted per php-cs-fixer and added a backslash-before-token regression test. |
||
|
|
c71714b07d |
security: preserve template quoting for raw path tokens in substitute_script_path
Keep the develop fix in sync with the 1.2.x backport (#8238): substitute_script_path() only sheds/closes the template's quotes for self-quoting (cacti_escapeshellarg) values. Trusted raw path_* tokens are substituted in place so a quoted "<path_php_binary>" keeps its quotes for the shell and a bare <path_cacti>/scripts/x.php stays unquoted for the PHP script server, which resolves the first token as a filesystem path. Reformatted per php-cs-fixer and added a raw-path-token regression test. |
||
|
|
79da94218b |
security(batch12): make substitute_script_path fully quote-aware (GHSA-5v3j)
The matched-pair quote shed only neutralised a token the template wrapped
exactly ("<arg1>"). A token embedded in a larger quoted word ("prefix<arg1>",
"<arg1>suffix", "<a><b>") kept the outer double quotes, so backticks/$(...) in
the already-escaped value stayed live - the bypass Copilot flagged.
Replace the regex with a single-pass shell-quote-state scanner: it still sheds
an exact wrapping pair, but for any token inside a larger quoted word it briefly
closes the template's quoting around the escaped value so it cannot be
re-interpreted. Values are never re-scanned (single-pass, GHSA-fq9x retained).
Updates the batch-11/12 source assertions, adds prefix/suffix/mixed regression
cases, and drops the Batch12 eval fallback (the unit bootstrap already loads
lib/functions.php via include/global.php).
|
||
|
|
6618f02d0c |
security(batch12): shed matched quotes around resolved data-input tokens (GHSA-5v3j-wcrr-jxjg)
Field values are escaped with cacti_escapeshellarg(), which wraps them in single quotes. When a Data Input template author placed the <field> placeholder inside a matched pair of quotes (e.g. "<arg1>"), those surrounding quotes made the value's own quoting literal, so $(...) or backticks in the value executed via the shell on the next poll - a realm-3 user reaching OS command execution, bypassing the CVE-2026-39902 hardening and not addressed by the single-pass fq9x fix. substitute_script_path() now consumes one matched pair of surrounding quotes when a token resolves, so the escaped value's own quoting is authoritative. Unresolved tokens keep their literal form (including any quotes), and the single-pass behaviour that fixed GHSA-fq9x is retained. Affects released 1.2.x (<= 1.2.31) and develop; this is the develop fix, to be backported to 1.2.x. |
||
|
|
ce707ccd79 |
Fix false-positive RRA-escape warning when path_rra has a trailing slash (#8258)
* Fix false-positive RRA-escape warning when path_rra has a trailing slash
A path_rra setting saved with a trailing slash makes CACTI_PATH_RRA end in
'/', so the '<path_rra>/' -> CACTI_PATH_RRA . '/' expansion that every data
source consumer performs yields a doubled slash ('<rra>//0/1457.rrd').
data_source_path_within_rra() split the expanded path on '/' and rejected any
empty segment, so the doubled slash was read as a traversal and a perfectly
legitimate RRD file was reported as escaping the RRA directory:
SECURITY: Data source 1457 has a data_source_path that escapes the RRA
directory (<path_rra>/0/1457.rrd). Using the generated path instead.
get_data_source_path() then returned the generated fallback path, which the
poller had never created, producing the follow-on 'RRD file does not exist'
graph warnings (note the tell-tale 'rra//0' doubled slash in those lines).
Collapse repeated slashes and strip a trailing slash from the RRA root before
the containment comparison. A '//' is an empty segment, not a parent
reference, so normalising it keeps the file contained while '..' traversal,
absolute paths elsewhere, lookalike prefixes and symlink pivots are still
rejected (asserted by the expanded regression test).
* Keep data_source_path_within_rra() normalisation strictly string-typed
preg_replace() is typed array<string>|string|null, so feeding its result to
rtrim()/strlen() tripped PHPStan (level 6/8). Cast CACTI_PATH_RRA and both
preg_replace() results to string; runtime behaviour is unchanged.
* Cover the trailing-slash CACTI_PATH_RRA half of the fix in an isolated process
The existing cases insert '//' into the target only; data_source_path_test_base()
uses realpath(), which strips the trailing slash, so they never exercise the
rtrim() on \. CACTI_PATH_RRA is immutable, so define it with a trailing
slash in a fresh PHP process and assert a clean/doubled-slash file stays
contained while a traversal is still rejected.
---------
Co-authored-by: TheWitness <1439914+TheWitness@users.noreply.github.com>
|
||
|
|
b5a388094d |
Honor @ error suppression in CactiErrorHandler on PHP 8 (#8197)
* Honor @ error suppression in CactiErrorHandler on PHP 8 CactiErrorHandler() guarded against the @ operator with error_reporting() == 0, which was the PHP 7 semantics. On PHP 8 the @ operator sets error_reporting() to a non-zero bitmask that merely excludes the suppressed level, so the guard never matched and deliberately suppressed warnings still reached the log. This surfaced after the Symfony mailer swap: Symfony's SendmailTransport writes to the sendmail pipe with @fwrite(), and when sendmail is misconfigured the broken-pipe notice leaked into the Cacti log instead of being swallowed and reported as the handled TransportException. Mask error_reporting() against the incoming level so the @ operator is honored on every supported PHP version. * Add regression test for CactiErrorHandler @-suppression mask Exercises both paths of the error_reporting() mask check: a notice and a warning raised under PHP 8's @-operator bitmask return before logging, while an enabled level is logged and reported back to PHP as unhandled. The suppressed cases would have logged under the previous error_reporting() == 0 guard, so the test fails closed if that regression returns. |
||
|
|
75d816a335 |
security: consolidate GHSA batch 11 fixes for develop (fq9x, hh7r, v99q, f5f5) (#8165)
* security: consolidate GHSA batch 11 fixes for develop (fq9x, hh7r, v99q, f5f5) Mirrors the 1.2.x batch-11 security consolidation. Items already fixed on the develop tip are credited on their advisories and omitted here: - GHSA-54hx-hxc4-jq7j: TICK/SHIFT values already pass through strip_alpha(). - GHSA-hh7r CSV portion: already guarded via cacti_csv_cell(); only the Content-Disposition filename needed sanitizing (included below). - GHSA-mx5c: REST scaffold already deny-by-default (404 unless api_enabled), enforced by tests/Unit/ApiDenyByDefaultTest.php. - GHSA-rjvj/pjq5/fmhq/mfw8: already addressed on develop. Fixes applied here: - GHSA-fq9x-x3vf-3vf2 (High): add substitute_script_path() single-pass token substitution and refactor get_full_script_path()/get_full_test_script_path() to use a pre-escaped value map, closing the second-order re-substitution OS command injection. - GHSA-hh7r-464j-rf39: sanitize the Content-Disposition filename in graph_xport.php. - GHSA-v99q-57w7-mpmg: escape the page title rendered in the HTML <title>. - GHSA-f5f5-h85v-6m3h: render user admin tooltip content as escaped text. * security: add batch-11 consolidation regression tests for develop Addresses PR #8165 review feedback by adding regression coverage for all four fixed sinks: - GHSA-fq9x: behavioral single-pass proof that substitute_script_path() does not re-substitute a field value containing another field token, plus source guards that both path builders use the helper. - GHSA-hh7r: Content-Disposition filename strips CR/LF/quote/backslash. - GHSA-v99q: the HTML <title> is html_escape()'d. - GHSA-f5f5: both the user_admin.php and user_group_admin.php tooltips render data-tooltip as escaped text. * style: apply php-cs-fixer formatting to substitute_script_path (CI coding standards) * docs: require composer run-script php-cs-fixit before creating any pull request * docs: require PHPStan (level 8, satisfies Level 6 floor) to pass before creating any pull request --------- Co-authored-by: Larry Adams <TheWitness@users.noreply.github.com> |
||
|
|
41e6d9e033 |
security: consolidate verified GHSA fixes for develop (batch 4) (#8145)
* security: consolidate verified GHSA fixes for develop (batch 4) Develop counterparts of the 1.2.x batch-4 fixes in #8144, re-verified against current develop HEAD. - GHSA-4rmr-wvjq-qxc2: include/auth.php Basic Auth and guest branches fall through to the per-page realm check instead of returning early. - GHSA-j67j-wpm4-9g3x: the pre-dispatch update_policy branch in user_admin.php and user_group_admin.php requires a CSRF-token POST. The denylist -> allowlist redesign is tracked for 1.3 in #8143. - GHSA-2p3x-f5jm-rg6p: the View-Graphs -> poller RCE chain is broken by the realm check (4rmr), the update_policy guard (j67j) and shell-free webhits (x5pw). - GHSA-x5pw-cg7j-mj4r: scripts/webhits.pl runs wc through a shell-free list-form open. - GHSA-9737-rqh3-h7fh: names interpolated into raise_message() now use __esc() across the UI; URL- and HTML-bearing messages are excluded. - GHSA-9x9h-2577-9w86: the colordropdown input is built as a node and the label is no longer round-tripped through parseHTML (the list item already used createTextNode on develop). - GHSA-h96j-8xhc-2p3w: remote_agent.php authorizes on $_SERVER['REMOTE_ADDR'], not get_client_addr(), which honours client-settable proxy headers. Regression guards under tests/Unit/Security/{Auth,Csrf,Xss,ShellInjection}. php -l, check-changelog.py and the sink/architectural baselines all pass. * chore: reference PR #8145 on the batch-4 1.3.0-dev CHANGELOG entries * security: resolve develop merge - keep id-only xmlerror unescaped (matches #8141 h745 test) * security(GHSA-j67j): use REQUEST_METHOD POST guard instead of raw \ (hardening-pattern-guard) * security(GHSA-9737): fix review findings - drop double-encoding in __esc, escape missed name sinks, correct plugins format-arg count |
||
|
|
3ab4c69fb7 |
PHPStan Level 8 - Phase 1 Conversion (#8135)
* PHPStan Level 8 - Phase 1 Conversion
Reduces the real (no-baseline) PHPStan level 8 error count from ~2,724 to 425
and regenerates phpstan-baseline.neon accordingly (8,941 -> 1,921 lines).
Core type-safety changes
- lib/database.php: narrow db_fetch_row()/db_fetch_row_prepared() to return
array (was bool|array) and db_fetch_assoc()/db_fetch_assoc_prepared() to
return array (was mixed); the _prepared workers coerce the connection-failure
path to an empty array so the declared type is honest. empty()/cacti_sizeof()
checks are unaffected.
- lib/database.php: narrow $db_conn guards from is_object() to instanceof PDO in
the transaction/quote/insert-id helpers so the PDO calls resolve.
- lib/mib_cache.php: the fluent MibCache methods mib()/object()/table()/row()
now return self instead of a bare object so chained calls type-check.
Type-safety casts and guards (mechanical, behaviour-preserving)
- Cast string|false/|null builtin results feeding string parameters
(json_decode, base64_encode, trim, htmle, preg_replace, explode, etc.).
- Cast int|string / int|false values feeding int id/pid parameters.
- Guard resource|false fopen()/popen() handles before fwrite/fclose/fread, and
wrap foreach over array|false/list|false results with `?: []`.
Genuine bugs found and fixed
- cli/add_tree.php / lib/api_tree.php: site tree nodes could not be created via
the CLI - the --site-id option was never parsed, $nodeTypes had no 'site'
entry (passing null to api_tree_item_save(int $type) -> TypeError), and
api_tree_item_save() never persisted site_id. All three fixed.
- tree.php: leaves_exist() is typed : int but returned db_fetch_assoc_prepared()
(an array of rows) for a SELECT COUNT(*); now uses db_fetch_cell_prepared()
and returns the scalar count.
- lib/mib_cache.php: $entries_per_object was a float (division / ceil) used as
an array key (PHP 8.1 implicit float-to-int key deprecation); now cast to int.
Config
- .phpstan.neon: add notIdentical.alwaysTrue and booleanAnd.alwaysFalse to the
existing always-true/false ignore family (same "PHPStan gets confused"
dynamic-constant cause already covered by identical.alwaysTrue,
notEqual.alwaysTrue, booleanAnd.leftAlwaysTrue, etc.).
- phpstan-baseline.neon: regenerated; the remaining 425 entries are the
untouched long tail for Phase 2.
* Remove redundant @phpstan-ignore comments
Follow-up cleanup for the level 8 Phase 1 work. Removes 14 inline
@phpstan-ignore comments that no longer suppress any reported error:
- cli/repair_database.php: `@phpstan-ignore booleanAnd.alwaysFalse` is now
covered by the global booleanAnd.alwaysFalse ignore added to .phpstan.neon.
- 13 others (host.php, include/global.php, include/session.php,
lib/api_automation.php x2, lib/functions.php x2, lib/html_utility.php x2,
lib/installer.php, lib/package.php, lib/reports.php, lib/rrd.php) no longer
match any reported error after the type-safety fixes (e.g. the
db_fetch_row_prepared() return-type narrowing removed the offset-assign
errors the api_automation ignores were suppressing).
Verified with reportUnmatchedIgnoredErrors enabled: 0 unmatched ignores
remain and the full level 8 run still reports 0 errors.
* ci: satisfy validator ratchet and refresh security sink baselines
- lib/api_tree.php: give the new tree-item site_id a real numeric
constraint ('^[-0-9]+$', matching html_reports.php) instead of an
empty regex, so the validator constraint ratchet passes without
bumping its baseline.
- tests/security/baselines: refresh the three sink/hotspot signatures
whose code text changed from the (string) casts added in this branch
(lib/poller.php setsid, cli/rrdresize.php json_encode, lib/import.php
simplexml_load_string).
* ci(js): allow DOMPurify 3.4.16 and update sync_js pin test
The dependabot bump (#8134) moved package.json/package-lock.json to
DOMPurify 3.4.16 on develop but left tests/js/sync_js.test.mjs pinned to
3.4.15, so the PR merge commit fails the 'DOMPurify is pinned' contract.
Bring package.json/package-lock.json to 3.4.16 (matching develop) and
update the pin test to expect 3.4.16.
* test: update sql.php null-handling assertion for the (string) cast
This branch changed scripts/sql.php from trim(\ ?? '') to
trim((string) \) (the phpstan-safe form that coerces both null and
false from shell_exec()). Update SqlScriptsTest's null-return assertion
to match the new cast instead of the removed '?? ''.
* fix: address Copilot review - ownership IDs, fopen guards, mib_cache bound
- chown()/chgrp(): pass integer UID/GID from fileowner()/filegroup()
(guarding the false lookup) instead of string-casting them, which made
them resolve account/group names like '1001' and fail. Covers
lib/api_device.php, lib/boost.php and poller_maintenance.php (x2).
- fopen() failure now short-circuits instead of reporting success:
lib/package.php returns false, lib/clog_webapi.php and utilities.php
render an error, and snmpagent_mibcachechild.php skips the cache write
and sentinel unlink so a concurrent writer's lock is preserved.
- lib/mib_cache.php: keep the float quotient as the loop bound and cast
only the array offset, so a non-even entries/columns split no longer
truncates the loop and drops a cached row.
* revert: keep db_fetch_* returning false on error; baseline Level 8 instead
Per review, restoring the intentional fail-safe: db_fetch_row[_prepared]()
return bool|array and db_fetch_assoc[_prepared]() return mixed again, so a
genuine SQL error (not just a missing connection) is signalled as false and
the === false checks in cdef_is_in_use(), boost_process_* and aggregate.php
work as before. Dropped the is_array() ? : [] coercion.
Level 8 stays green by regenerating phpstan-baseline.neon to capture the
re-introduced bool|array/mixed call-site errors (the ~1,850 the narrowing
had eliminated). Tightening those callers to drop them from the baseline is
Phase 2 work.
---------
Co-authored-by: TheWitness <copilot@users.noreply.github.com>
|
||
|
|
a52368125e |
feat(auth): rename User Domains to Login Providers, add SAML2 and OpenID Connect (#8066)
* feat(auth): rename User Domains to Login Providers Collapses user_domains/user_domains_ldap into a single login_providers table with a JSON parameters column, migrated in place during the 1.3.0 upgrade. Renames the admin page, menu, breadcrumbs, and help links to match. * feat(auth): rework login providers as Cacti\Auth classes Introduces LoginProviderInterface with credential (LDAP/AD) and redirect (SAML2/OpenID) flavors, replacing the procedural domains_login_process()/domains_ldap_*() functions. Adds blank-field group membership gating shared by every provider type. * feat(auth): add SAML2 and OpenID Connect login providers Adds onelogin/php-saml and firebase/php-jwt, a new cacti_http() SSRF-hardened outbound request helper for every OpenID discovery/token/JWKS call, and login_sso.php to handle the redirect-based SSO flow. Login page now shows a button per enabled SAML2/OpenID provider, and per-provider auth cookie opt-out is enforced on both the password and SSO login paths. * chore(i18n): regenerate cacti.pot * feat(auth): add Local and Basic auth as Cacti\Auth providers LocalAuthLoginProvider (CredentialLoginProviderInterface) and BasicAuthLoginProvider (new PreAuthenticatedLoginProviderInterface, since Basic Auth has no password for Cacti to check) are thin adapters over the existing local_auth_login_process()/basic_auth_login_process() functions, which are left untouched to avoid disturbing their regression-tested lockout/timing/rehash behavior. * feat(auth): move local password verification into LocalAuthLoginProvider secpass_login_process() and local_auth_login_process() are retired; their lockout, constant-time verification, forced-password-change, and rehash-on-login logic now live end to end in the class (verifyCredential/enforcePasswordPolicy/rehashIfNeeded). secpass_check_pass() stays a shared lib/auth.php function since password change/reset/user_admin also call it outside any login flow. Local auth still has no login_providers row: isEnabled() is always true and it cannot be disabled or deleted. * feat(auth): move Login Provider CRUD into Cacti\Auth classes AbstractLoginProvider gains static deleteById()/enableById()/disableById()/makeDefaultById()/persist(); each concrete provider owns its own collectParameters(), dispatched via LoginProviderFactory. login_providers.php's form_save()/form_actions() call these instead of doing raw SQL, and the retired provider_remove()/provider_disable()/provider_enable()/provider_default()/provider_collect_parameters() procedural functions are gone. Local/Basic auth are unaffected: they never extend AbstractLoginProvider, so they structurally cannot expose delete/disable. * fix(auth): address Copilot review findings on SSO/SAML/OIDC security Fixes callback/ACS/SLS/metadata URLs that used the raw provider id instead of the login realm (broke every SSO login), adds InResponseTo correlation for SAML and sub/azp validation for OIDC, requires HTTPS for OIDC discovery/endpoints, pins cacti_http() connections against DNS rebinding via CURLOPT_RESOLVE, exempts login_sso.php from the authenticated-page redirect guard and gates it on auth_method, wraps provider dispatch in a exception boundary, adds a dedicated SAML SLO path instead of routing it through login completion, makes remember-me an explicit opt-in for SSO, fixes disabled/deleted providers not revoking existing auth cookies, restricts the default-realm action to LDAP/AD, and fixes the 1.3.0 migration silently enabling group enforcement for installs that had it off. Also updates the E2E seed script for the login_providers schema. * chore(auth): refresh CI security baselines and document no-op validation Regenerate the sink inventory baseline for the new/renamed lib/Auth/* and login_providers.php files, and add '// no-validation:' comments on the intentionally-unconstrained admin/free-text/secret fields (LDAP DNs/filters, SAML certs/keys, OIDC client credentials, provider display fields) flagged by the validator constraint ratchet. Refreshes the validator baseline to drop the deleted user_domains.php entries. * refactor(ldap): remove dead DB-lookup path from Ldap::__construct The domain_id>0 branch duplicated the JSON-parameter-to-property mapping already owned by LdapLoginProvider::buildLdap(), and nothing constructs Ldap with a real id anymore (all call sites pass 0). Drop the parameter and the branch so Ldap stays a pure connection/protocol class with no DB coupling. Also fixes a latent bug: group_require is a typed bool property, but LdapLoginProvider::authenticate() assigned it an int (1/0) from a strict_types=1 file, which throws a TypeError on every LDAP/AD login attempt. Assign a real bool instead. * fix(auth): fix LDAP login, upgrade ordering, and hardening guard failures LdapLoginProvider compared LdapError::GetErrorDetails()'s int error_num against the string '0' with strict equality, so it never matched and every LDAP/AD login was rejected regardless of valid credentials. Compare against int 0 instead. install/upgrades/1_3_0.php called login_providers_convert_1_3_0() (which reads and then drops user_domains/user_domains_ldap) before the pre-existing legacy ALTER TABLE statements that add the debug/network_timeout/bind_timeout columns those reads depend on, so upgrades failed once the tables were already gone. Moved the call to run after those ALTERs. OpenIdLoginProvider::complete() read state/code from raw \ instead of gnrv(), tripping the hardening pattern guard; switched to the request helper. * style(auth): apply php-cs-fixer formatting and normalize line endings to LF Runs php-cs-fixer across the new lib/Auth/* classes, login_providers.php, login_sso.php, and the other touched files, fixing indentation/alignment/docblock style to match project convention. Also normalizes several files that had picked up CRLF line endings back to the LF the project's .php-cs-fixer.php config (setLineEnding) and the rest of the tracked tree actually use. * fix(auth): address second Copilot review round (9 high/medium findings) - Make login_providers_convert_1_3_0() resumable: CREATE TABLE IF NOT EXISTS and INSERT ... ON DUPLICATE KEY UPDATE so an interrupted upgrade can be re-run instead of leaving a half-migrated, unrecoverable table. - SamlLoginProvider::complete() now fails closed when no saved AuthnRequest ID exists instead of passing null to processResponse(), which skipped InResponseTo validation and accepted unsolicited IdP-initiated assertions. - LoginResult gains an isCredentialFailure flag; only a genuine wrong-password LDAP bind (LdapError::Failure) counts toward the lockout counter now, not connection errors or group-membership denials. - Fixed button_label/debug being read from the JSON parameters column in OpenIdLoginProvider/SamlLoginProvider/LdapLoginProvider when they are actually top-level login_providers columns; AbstractLoginProvider now hydrates and exposes both. - Removed SamlLoginProvider's wantMessagesSigned, which silently required the outer SAML message (not just the assertion) to be signed, rejecting IdPs that only sign the assertion as the UI promises. - Added getTemplateUserId() to CredentialLoginProviderInterface and implemented it on LocalAuthLoginProvider so the contract (not just incidental AbstractLoginProvider inheritance) guarantees the method lib/auth.php calls. - cacti_http()'s CURLOPT_RESOLVE entries now bracket IPv6 literals correctly, and safe-IP validation strips parse_url()'s brackets before checking, fixing IPv6 target pinning. - login_providers.php's type dropdown now swaps the Full Name attribute default (cn/displayName) on an actual user-driven type change, not on page load, so a new Active Directory provider does not silently save the LDAP default. - Extracted auth_apply_group_login_opts() and call it from both auth_login.php and login_sso.php, so SSO logins honor a user's group-derived login_opts the same way password logins already do. * fix(auth): fix PHPStan level 8 errors and pest harness stub drift Test harness fix: the LoginProvidersLoginRealmValidationTest stub LoginResult class was missing the isCredentialFailure property added to the real class, causing 'Undefined property' warnings that broke the harness's JSON output. Added the property and set it on the simulated bad-password failure. PHPStan (48+ errors, all newly introduced by this branch's rewrite, never actually verified until now): narrowed db_fetch_row_prepared()/db_fetch_assoc()'s array|bool (including bare true, which survives a '?: []' short-circuit - fixed with explicit is_array() checks instead) at every call site across auth_login.php, login_sso.php, lib/auth.php, lib/Auth/LocalAuthLoginProvider.php, lib/Auth/LoginProviderFactory.php, and install/upgrades/1_3_0.php. Fixed OpenIdLoginProvider::discover()'s return shape to match its declared docblock instead of returning the raw decoded JSON. Fixed lib/functions.php's cacti_http() to narrow curl_exec()'s string|bool result with is_string() (not ===false, which still leaves 'true' as a residual type) and guard / as non-empty for curl_setopt_array()'s stubs. Fixed a real bug in SamlLoginProvider::processLogout(): processSLO(true) was passing 'true' as \ (the first parameter) instead of \ (the fifth), so it always redirected/exited internally with the default \=false, making Cacti's own logout.php never regain control. Now calls processSLO(keepLocalSession: true, stay: true) and is typed ': never' to match; login_sso.php's now-genuinely-unreachable break statements after processLogout()/initiate() are removed. Fixed login_providers.php passing a expression (not a variable) to inject_form_variables()'s by-reference parameter. Verified: PHPStan analyse clean (0 errors), full Pest Unit/Security suite passes (128 tests), hardening guard/sink inventory/validator ratchet all still clean. * fix(auth): fix ambiguous column and array+bool crash in user_log filter The user filter dropdown query selected a bare 'id' column after joining user_auth and login_providers, which is ambiguous now that login_providers also has an id column (unlike the old user_domains.domain_id) - MySQL rejected the query outright. Qualified as ua.id. Also narrow db_fetch_assoc()'s array|bool result before the \ + \ + \ merge, which was throwing a TypeError whenever the query failed instead of surfacing the real SQL error. * fix(auth): address third Copilot review round (5 findings + tests) - AbstractLoginProvider::persist() now disables the template account for every provider type; SAML2/OpenID also copy it via user_copy() in login_sso_complete() and were left directly usable as a local login. - cacti_http_resolve_safe_ips() now detects IPv4-mapped IPv6 literals (::ffff:0:0/96) via inet_pton() and validates the embedded IPv4 address, closing an SSRF bypass via e.g. ::ffff:169.254.169.254. - login_providers_convert_1_3_0() now tracks db_install_execute()'s status for every row and only drops the legacy user_domains/user_domains_ldap tables once every row (and the final row count) is confirmed migrated. - Ldap::Getcn() now looks up ldap_get_entries()'s always-lowercased attribute keys case-insensitively while still storing the result under the originally-requested key, fixing AD's displayName (and any other mixed-case configured attribute) always resolving to a blank full name. - SamlLoginProvider::processLogout() now redirects to the LogoutResponse URL processSLO() returns for a LogoutRequest (stay:true), instead of discarding it and always redirecting to Cacti's own logout page - federated logout was never actually acknowledged to the IdP. - Added tests/Unit/Security/Auth/OpenIdLoginProviderTest.php (13 tests) covering verifyIdToken() signature/issuer/audience/azp/nonce validation, discover()'s HTTPS enforcement, and complete()'s callback-state and UserInfo-subject-matching checks, against a locally-generated RSA keypair/JWKS. - Regenerated the sink inventory baseline for the new header_redirect call in SamlLoginProvider::processLogout(). Verified: PHPStan clean, full Pest Security/Auth suite passes (830 tests including the 13 new ones), php-cs-fixer/hardening-guard/sink-inventory/validator-ratchet all clean. |
||
|
|
38c1d05f08 |
perf: cache realpath(CACTI_PATH_RRA) in data_source_path_within_rra() (#8070)
* perf: cache realpath(CACTI_PATH_RRA) in data_source_path_within_rra() * perf: align RRA-root realpath cache with 1.2.x's array-keyed pattern |
||
|
|
2d516ca0a9 |
security: consolidate additional verified GHSA fixes for develop (batch 2) (#8065)
* security: contain data source paths within the RRA directory (GHSA-9qff-xccr-rrc5)
* security: escape executable path in exec_background on non-win32 (GHSA-cv7j-p6fh-j4q7)
* security: stop disabled accounts authenticating via remember-me cookie and revoke on disable (GHSA-pqrp-v752-mc4f)
* security: prefer base_url over client Host for the force_https redirect (GHSA-3466-fp7g-c9rf)
* changelog: add fragments for GHSA-9qff-xccr-rrc5, GHSA-cv7j-p6fh-j4q7, GHSA-pqrp-v752-mc4f, GHSA-3466-fp7g-c9rf
* security: address review findings on GHSA batch 2 (develop)
- lib/dsstats.php, poller_maintenance.php: route through
get_data_source_path() instead of expanding data_source_path directly,
so the RRA containment check applies to these consumers too (GHSA-9qff)
- lib/functions.php: data_source_path_within_rra() now also validates
the realpath of each existing ancestor segment, closing a symlink
pivot that the purely lexical check missed (GHSA-9qff)
- include/global.php: fail closed instead of falling back to SERVER_NAME
when base_url is unset/invalid, removing the residual open-redirect
path (GHSA-3466)
- lib/html_utility.php: cacti_force_https_host()/
cacti_build_https_redirect_url() now parse a scheme-less base_url
(matching lib/reports.php's own normalization) and preserve a
configured non-default port instead of silently dropping it (GHSA-3466)
- tests: move DataSourcePathContainmentTest.php into
tests/Unit/Security/PathTraversal/, and switch its fixtures from a
fictional CACTI_PATH_RRA path to a real temp directory so the new
realpath()-based checks (including a symlink-pivot regression case)
can actually run
* test: fix CACTI_PATH_RRA constant collision in DataSourcePathContainmentTest
CI showed 2 failures: RrdProxyProtocolTest.php runs earlier in the same
Pest process and already defines CACTI_PATH_RRA to a fictional,
non-existent '/var/lib/cacti/rra', so our own definition guard never
ran and realpath(CACTI_PATH_RRA) returned false for every assertion,
failing the two tests that expect containment to be true.
Now ensure the directory backing CACTI_PATH_RRA actually exists on
disk regardless of who defined the constant first, and gracefully
skip only the fixture-dependent assertions if this process can't
create it (e.g. no permission on a system path).
* test: refresh sink inventory baseline for exec_background rename
|
||
|
|
1e5329dd89 |
security: consolidate verified GHSA-rjvj win32 cmd.exe hardening for develop (#8056)
* fix(win32): harden the cmd.exe shell sinks on develop (GHSA-rjvj-r52f-8v5q) Forward-port of the 1.2.x win32 command-injection remediation. On Windows the SNMP/ping/data-query/trap commands run through cmd.exe, which ignores the \" escape and toggles quoting on every ", so cacti_escapeshellarg cannot stop & | ^ < > ( ). - add cacti_escapeshellarg_cmd(), stripping those operators on win32 before quoting (a Unix pass-through) - snmp_escape_string(): strip the operators in the win32 branch - route the SNMP binary hostname, the data-query device values (get_script_query_path), the data-input arg builders, the Windows ping hostname, and the SNMP trap notification arguments through the strip get_script_query_path is device-reachable (a rogue monitored device supplies an SNMP index/system field that executes on the Windows poller); the rest are post-auth. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(win32): strip %VAR% from the hostname on Windows (GHSA-rjvj-r52f-8v5q) cmd.exe expands %VAR% even inside quotes, so a device hostname of %USERNAME% would expand into the SNMP/ping command line. Add an opt-in $strip_env flag to cacti_escapeshellarg_cmd() and set it for the SNMP binary and ping hostnames, which never legitimately contain a percent. Community and credential values are left untouched because they can legitimately contain %. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): note win32 cmd.exe hardening for develop * security: require Package Management permission to trust new package signers package_accept_key() (reachable via the repository-based package import flow) inserted new keys into package_public_keys, the global trust store consulted by import_validate_signature() for every future import by every user, without checking for the Package Management (realm 29) permission that gates package_keys.php/package.php/package_repos.php for the same table. An Import Templates (realm 17) user could therefore add themselves as a trusted signer for the whole install. Also add the 'accept' action to the CSRF-protected bad_actions list so the acceptance step requires POST plus a valid token like every other state-changing action. GHSA-gxgm-8rhv-r7jv * docs(changelog): note package-signer trust authorization fix * security: close tab-based open redirect bypass in CSRF timeout handler sanitize_uri()'s guard ( |
||
|
|
ac0b2c9515 |
Fix confirmed poller, database, automation, and UI defects (#7861)
* issue#7024: do not delete still-running poller_time rows on cycle start The overrun path deleted every poller_time row for the poller, including the in-flight ones it had just counted, so overrunning processes lost their tracking. Restrict the delete to completed rows. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7028: restore identifier and connection safety in the db layer _db_replace interpolated caller-supplied column keys as backtick identifiers without escaping, and db_commit_transaction checked @@in_transaction on the default connection rather than the one passed in. Both were correct on 1.2.x and regressed on develop. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7341: size the CSV export maxrows to the requested span The export hardcoded --maxrows=10000, truncating wide date ranges regardless of the data source profile step. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7421: offset automation_get_next_host through the 32-bit integer The hand-rolled octet walk subtracted the wrong magnitude per octet and emitted out-of-range octets for ranges wider than a /16. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7030: skip the poller_output ENGINE=MEMORY conversion when already applied The output-table refresh ran ALTER TABLE poller_output ENGINE=MEMORY every cycle even though the swapped-in copy already inherits the engine, adding a metadata-locking DDL to the hot path. Probe the current engine and convert only when needed. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7342: prefer snmpEngineTime only when it covers system uptime update_system_mibs preferred snmpEngineTime (x100) over sysUpTime whenever it was present, so an SNMP engine that restarts independently of the system (for example OpenBSD snmpd) made the stored uptime drop, which reads as a reboot and forces a perpetual reindex with mass 'U' results. Capture both values and use snmpEngineTime only when it is at least the system uptime, which still covers the 32-bit sysUpTime wrap. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7403: scale lmsensors readings in the get path like the query path The get path returned the raw milli-unit SNMP counter while the query path divided voltage and temperature by 1000 (and corrected negative voltages), so a get gave values 1000x too large. Apply the same scaling before returning. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7029: propagate the reconnected handle back to the caller db_check_reconnect() closed the dead connection, opened a new one, and returned true without giving the new handle back, so the db_execute_prepared retry ran its query against the connection that was just closed. Take $db_conn by reference and assign the fresh handle; the one caller that passed a literal false now passes a variable. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: refresh sink inventory baseline Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: refresh architectural hotspots baseline Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7474: return valid after correcting the theme to selected_theme is_valid_theme() corrected an invalid theme to selected_theme but left $valid false, so callers saw the theme as invalid even though it had been fixed. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7462: allow SELECT aliases as sortable automation columns site_name and host_template_name are aliases (s.name AS site_name), valid in ORDER BY but not real columns, so api_automation_column_exists() rejected them and the site-name sort silently failed. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7456: bind the OS value in the automation OS filter The OS filter built 'os = ?' but bound the network value, so filtering by OS matched on the wrong input. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7491: filter host_template_id against its own column html_make_device_where() compared h.location to the host_template_id value instead of h.host_template_id. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7479: apply the hostname and description filters in get_hosts The REST get_hosts() honored host_id/poller/site/template/location but ignored the hostname and description filters its callers pass; add them as bound LIKE. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * issue#7475: authorize a direct client-IP poller match before the hostname guard A poller configured by IP with no hostname-based pollers was rejected because the empty-hostname guard ran before the direct-match check, even though the client address exactly matched a poller's configured address. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Guard two divisors that PHP 8 makes fatal Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: consolidate functional fix changelog * test: cover consolidated functional fixes * docs: identify consolidated functional fixes --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
964c1fb1f4 |
Harden RRDtool execution and data handoff (#7615)
* fix(rrdtool): quote RRDfile paths passed to the command line __rrd_execute() hands the assembled line to shell_exec(), and its docblock already required callers to quote each variable argument. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(tests): widen bare RRD path sweep Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(rrdtool): quote binary path as one argument Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(rrd): preserve caller logging context Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Make RRD XML mutations transactional Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Harden RRDtool proxy transport handling Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(rrdproxy): reject invalid command argument lists Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(rrdproxy): preserve binary graph payloads Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Keep RRDtool proxy sessions encrypted Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Harden local RRDtool command and output handoff Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Eliminate graph rendering metadata lookups Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Batch DSStats metadata and last-value reads Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Batch interface speed metadata reads Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Update architectural hotspot baseline for RRD execution Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Batch RRD resolution profile lookups Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Batch DSStats summary aggregates Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Narrow phpseclib key types for the proxy encrypt and decrypt calls Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(rrd): bound the forked proxy peers so a stall cannot hang the suite The children accepted and read with no deadline while the parent used a 2s socket timeout, so any mismatch left the child blocked in read and the parent blocked in a bare pcntl_waitpid(). Note that accept() ignores SO_RCVTIMEO, so readiness has to be waited on with socket_select() rather than a socket option. Also corrects the array-command expectation, which still assumed develop's escape-everything behaviour rather than rrdtool_build_command() leaving the operation unquoted. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(rrd): seed proxy settings where the real read_config_option looks UnitStubs only declares its stub when lib/functions.php has not been loaded yet, so during a full run the real database-backed function is the one in effect and $unit_config_options was being ignored. The parent then dialled an address the child was not listening on, which is what stalled the handshake. Seeding the option cache keeps the settings visible to either implementation. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(rrd): assert the shell-free RRDtool launch instead of the old quoting __rrd_init now hands proc_open an argv array with bypass_shell, so escaping the binary path would put the quotes inside the filename. The assertion still expected the pre-argv line and had been failing unnoticed behind the suite hang. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(dsstats): derive the expected stamps instead of assuming UTC dsstats_poller_output() formats with date(), so the hardcoded UTC strings only matched on a UTC runner. The failure was masked by the suite hang. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(rrd): keep RRDtool control codes intact in stdin commands Escaping the backslash when converting a shell-quoted argument to an RRDtool token left legends such as " IO Wait\:" ending in a dangling control code, so RRDtool refused every graph carrying an escaped colon. Verified against RRDtool 1.7.2 in pipe mode; values with no unambiguous quoted form are now refused rather than mangled. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: use Cacti test bootstrap and config Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: resolve includes through Cacti path constants Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test: match the escaping helpers by token, not by text lib/rrd.php documents its escaping behaviour in a comment, and the regex over raw source counted that as a call site. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: refresh sink inventory after develop rebase Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(rrd): prove argv isolation and concurrent pipe draining Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * test(security): refresh sink inventory baseline after rebase onto develop Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
5727cebebd | security: forward-port 1.2.x hardening (dsstats, guest, permissions, cookie) (#7844) | ||
|
|
cdf92b7c8d | Resolve the client address through the trusted proxy list (#7658) | ||
|
|
4a98d27e6c |
security: escape external link titles and the tree data query index (#7815)
* hardening: escape the data query index in the tree graph header SNMP-collected values are stripped of <>"' by format_snmp_string(), so this is not exploitable through the SNMP path, but script-query data reaches host_snmp_cache.field_value without that filter. Escaping at the sink matches the sibling values on the same line. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * security: escape external link titles in the page title element external_links.title is stored with an empty validation regex and the title branch of draw_navigation_text() returned it raw. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
f487bc3530 |
fix(functions): cast path_php_binary before escaping it (#7792)
The forward-port in
|
||
|
|
619b33142e | feat(rendering): add HTML, URL, and inline-data output helpers (#7252) | ||
|
|
02386f5006 | security: escape path_php_binary in test_data_source (#7786) | ||
|
|
4d021a93e3 | Add plugin-extensible notification API (#7775) | ||
|
|
49f5212181 |
fix(tests): restore paths and isolate test doubles (#7764)
* fix(tests): correct path depths after the domain reorganisation The category move left nine files resolving paths relative to their old depth, so the bootstrap could not read tests/Helpers/UnitStubs.php and no test ran at all. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix tests without replacing production functions * remove unrelated failing integration tests * remove unrelated broken test workflows --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
d6f56fb51b | Keep an empty argument on the command line (#7695) | ||
|
|
96033267cb |
refactor(tests): reorganize unit tests into domain category folders (#7733)
* refactor(tests): reorganize unit tests into domain category folders - Consolidate per-advisory GHSA regression tests into Security/ subfolders by vuln class - Group all 186 test files into domain folders: DataCollection/, Ui/, Core/, Database/, Installer/, Scripts/, Security/, Plugin/ - Strip GitHub issue/GHSA numbers from filenames; keep IDs in test descriptions - Fix all __DIR__ relative paths to match new folder nesting depth - Update CI workflow path triggers and phpunit XML configs for new file locations - Update .github/copilot-instructions.md with unit test organization guidelines - Add tests/vendor/ to .gitignore * add gaurds * fix up guard for unit tests * fix redeclaration of cacti_log in tests * fix updates to unit tests * fix test patjhs * fix: update test __DIR__ paths for reorganized test structure * fix: conditionally define read_user_setting to prevent redeclaration in tests * fix: conditionally define functions to prevent redeclaration in tests - Wrap clean_up_lines, raise_message, cacti_debug_backtrace, and get_debug_prefix with function_exists() checks to prevent conflicts when test stubs are loaded before lib/functions.php in the same PHP process * fix: add unit test bootstrap to prevent function redeclaration errors - Create tests/bootstrap-unit.php to load core libraries before tests - Ensures lib/functions.php loads before test stubs define stub functions - Test stubs use function_exists() checks, so they detect pre-loaded functions - Update phpunit.xml to use bootstrap-unit.php for Unit testsuite - Fixes 'Cannot redeclare read_user_setting()' errors when tests run in same process * fix: load test stubs before global.php in unit test bootstrap - Reorder bootstrap to load CactiStubs.php and UnitStubs.php first - Ensures stub functions like __() are available when global.php loads - global.php calls get_cacti_version_text() which needs __() for translations - Fixes 'Call to undefined function __()' error during bootstrap * fix: remove function conflicts from test stubs - Remove stub definitions for functions already in lib/functions.php: cacti_sizeof, cacti_log, cacti_strtolower, read_config_option, read_user_setting, cacti_count - Keep translation function stubs (__(), __esc()) needed by global.php - Update bootstrap to load lib/functions.php after stubs - This prevents function redeclaration errors while maintaining test isolation * fix: prevent constant and variable redeclaration in bootstrap - Remove explicit PHP_TESTING and CACTI_TEST_BOOTSTRAP defines from bootstrap (CactiStubs.php now handles this with guards) - Add guards to CactiStubs.php to prevent redefinition of PHP_TESTING - Only set CACTI_TEST_BOOTSTRAP env vars if not already set - Fixes PHP warnings about 'Constant X already defined' * fix: remove redundant lib/functions.php load from bootstrap include/global.php already loads lib/functions.php at lines 287-288. The bootstrap was trying to load it again after global.php, but global.php needs format_cacti_version() to be available on lines 293-294 to define CACTI_VERSION and CACTI_VERSION_FULL constants. Solution: Let global.php handle loading lib/functions.php itself, and don't load it separately in the bootstrap. * fix: remove more conflicting function stubs from test helpers - Remove stubs for functions also defined in lib/functions.php: raise_message, clean_up_lines, cacti_debug_backtrace, get_debug_prefix - Remove POLLER_ID constant stub (defined unconditionally by global.php) - Keep only translation functions and test-specific helpers - This prevents function/constant redeclaration errors * fix: minimize test stubs to only translation functions Remove ALL helper functions and constants from UnitStubs.php that are: - Defined in lib/functions.php (raise_message, clean_up_lines, etc.) - Defined in lib/html_utility.php (srv, set_request_var) - Defined in global_constants.php (POLLER_VERBOSITY_*, MESSAGE_LEVEL_*) - Defined in global.php (POLLER_ID) Keep ONLY translation functions (__(), __esc()) that are: - Called by global.php during initialization - Not defined elsewhere - Required for proper bootstrap sequence This eliminates all constant and function redeclaration errors. * fix: remove __esc stub - already defined in global_languages.php Keep only __() translation function stub, which is needed by global.php and not defined elsewhere in Cacti core. * fix: handle no-arg case in __() translation stub The __() function may be called with just a string and no format arguments. Previously, vsprintf() would fail with 'arguments array must contain 2 items'. Now, if no args are provided, return the text as-is. |
||
|
|
d4e255b5d9 |
security: stage nonce CSP migration and gated enforcement (#7662)
* security: add nonce CSP mode for inline JavaScript protection Reworks the content_security_policy_script setting into a three-mode Inline JavaScript Protection dropdown (HTMLX default, Nonce, None) and ports the per-request nonce, strict-dynamic policy, and CSP violation report endpoint from 1.2.x. The stored option key and its default are unchanged, so existing installs keep their current policy byte-for-byte; nonce mode is opt-in. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * security: stage nonce CSP enforcement safely Add report-only migration, gated enforcement, nonce-aware shared emitters, Linux Docker validation, and operator documentation.\n\nRefs #7703 * security: validate CSP header sources Reject malformed alternate-source configurations at the CSP boundary, validate fallback report URIs, and remove the obsolete Psalm header-taint baseline entry.\n\nRefs #7705 * chore: drop the composer.json change from this PR Dependency and manifest changes are a maintainer call and do not belong in this one. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
cb79286b18 |
Contain plugin archive writes and validate the repository URL (#7656)
* Contain plugin archive writes and validate the repository URL Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Use the full Cacti copyright header and reject non-base repository URLs Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Move the path containment helpers into lib/functions.php They live in lib/functions.php on 1.2.x; keeping a separate module on develop split the same helpers across two files. plugin_validate_repository_url() goes to lib/plugins.php with its caller. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: refresh the sink baseline for the containment guard The archive guard shifts eleven sink line numbers in lib/plugins.php. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: drop the baseline refresh this branch did not need The sink and hotspot gates compare on a line-agnostic signature, so pure line drift never tripped them and refreshing only invites merge conflicts. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> |
||
|
|
c5fa3d2f75 |
Repair the string sanitisers in lib/functions.php (#7696)
* fix(functions): compare string offsets explicitly in sanitize_uri A graph_view.php URI opening with 'action=' or '?' matched at offset 0, which strpos() reports as a falsy 0, so it gained a duplicate token. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(functions): fall back when preg_replace returns null clean_up_lines() and sanitize_uri() were the two sites without the guard every sibling call in the file already had. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): note the sanitiser repairs Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(functions): tighten sanitizer contracts --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net> |
||
|
|
979c048ae3 |
security: block loopback/link-local SSRF targets in call_remote_data_collector (#7672)
* security: block loopback and link-local targets in call_remote_data_collector The function issued an outbound file_get_contents() to any hostname in the poller table with no address check, so an admin could point a remote collector at 127.0.0.1 or the cloud metadata endpoint (169.254.169.254) and turn a status poll into SSRF. The resolved target is now rejected when it falls in a reserved range (FILTER_FLAG_NO_RES_RANGE) before the fetch. RFC1918 private ranges stay allowed so distributed collectors on internal LANs keep working. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Drop the redundant null-coalesce on the resolved collector IP Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net> |
||
|
|
3ee093a112 |
Guard two divisions PHP 8 made fatal (#7697)
* fix(poller): guard the average against a zero divisor Counters that disagree made the divide fatal on PHP 8, and the reverse lookup used a failed write as the offset it parses the reply from. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): note the zero divisor guards Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * ci: drop the baseline refresh this branch did not need The sink and hotspot gates compare on a line-agnostic signature, so pure line drift never tripped them and refreshing only invites merge conflicts. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(poller): describe the PHP 7 divide result accurately It returned an infinity signed by the numerator rather than INF in every case. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net> Co-authored-by: Sean Mancini <sean@seanmancini.com> |
||
|
|
1f360b2420 |
fix log entry (two spaces) "fields which is NOT okay" (#7726)
Co-authored-by: TheWitness <thewitness@cacti.net> |
||
|
|
a6da63ac89 |
Harden remote graph transport and response validation (#7631)
* Harden remote graph transport boundaries * Tighten remote graph SVG validation * fix: PHPStan Issues with emerging code * Refresh security baselines after develop rebase Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * Match transport test to the explicit response-header default Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <thewitness@cacti.net> |