Skip to content

security: replace rand() with hrtime(true) for graph cache-buster (GHSA-vhwj-hfwg-gfg3) - #806

Merged
TheWitness merged 21 commits into
developfrom
advisory-fix-1
Sep 21, 2026
Merged

TheWitness merged 21 commits into
developfrom
advisory-fix-1

Conversation

@bmfmancini

@bmfmancini bmfmancini commented Aug 17, 2026

Copy link
Copy Markdown
Member

Security Fix — GHSA-vhwj-hfwg-gfg3

Fixes GHSA-vhwj-hfwg-gfg3

Summary

The threshold editing page in thold.php used PHP's rand() function (line 1278) to generate a cache-buster query parameter for a graph image URL. rand() is not a cryptographically secure pseudo-random number generator (CSPRNG).

Vulnerability Details

  • Rule: php:S2245 (SonarQube)
  • CWE: CWE-338 (Use of Cryptographically Weak Pseudo-Random Number Generator)
  • OWASP: A02:2021 - Cryptographic Failures
  • Severity: Major
  • Location: thold.php, line 1278

Changes

Replaced rand() with hrtime(true), a monotonic nanosecond clock that never throws.

Why not random_int()? An initial fix used random_int(0, PHP_INT_MAX), but reviewer feedback correctly identified that random_int() can throw Random\RandomException when the platform CSPRNG fails. This URL parameter is a cache-buster, not a security boundary, so a CSPRNG is the wrong reliability tradeoff — it would make threshold editing fatal merely to produce a non-security cache-buster.

hrtime(true) provides:

  • Never throws — no CSPRNG dependency
  • Monotonic — never goes backwards on NTP clock step
  • Nanosecond resolution — no collisions on fast page renders
  • Returns int — clean URL query parameter
  • Not a PRNG — side-steps php:S2245 entirely (no "random" function to flag)

Testing

  • PHP syntax check passes (php -l thold.php)
  • SonarQube scan confirms php:S2245 is CLOSED
  • SE:Security agent review: APPROVED — "best of the candidates; reviewer's CSPRNG rejection was correct"

Copilot AI lite review requested due to automatic review settings August 17, 2026 20:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

TheWitness
TheWitness previously approved these changes Aug 17, 2026

@somethingwithproof somethingwithproof left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

random_int() can throw Random\RandomException when the platform CSPRNG fails, which would make threshold editing fatal merely to produce a non-security cache-buster. This URL parameter is not a secret or security boundary, so a CSPRNG is the wrong reliability tradeoff. Please use a non-throwing cache version (for example a timestamp/monotonic value) or catch the exception, add a focused Pest assertion through Cacti Composer, and add the CHANGELOG entry. The integration matrix is currently red.

@bmfmancini bmfmancini changed the title security: replace rand() with random_int() for graph cache-buster (GHSA-vhwj-hfwg-gfg3) security: replace rand() with hrtime(true) for graph cache-buster (GHSA-vhwj-hfwg-gfg3) Aug 17, 2026
@somethingwithproof

Copy link
Copy Markdown
Member

The latest revision fixes the non-throwing cache-buster implementation, but the regression test still exercises mt_rand() while production now calls hrtime(true), so it cannot fail if the production line regresses. The CHANGELOG also contains both an mt_rand entry and an hrtime entry. Please make the test assert the actual thold.php behavior, keep one accurate CHANGELOG entry, and remove the unrelated duplicated workflow changes (including removal of the advisory develop matrix) from this security fix.

…245)

Replaces the non-cryptographic PRNG rand() with hrtime(true), a
monotonic nanosecond clock that never throws. This URL parameter is
a cache-buster, not a security boundary, so a CSPRNG (random_int)
is the wrong reliability tradeoff — it can throw Random\RandomException
on CSPRNG failure, making threshold editing fatal.

hrtime(true) provides:
- Never throws (no CSPRNG dependency)
- Monotonic (never goes backwards on NTP step)
- Nanosecond resolution (no collisions on fast page renders)
- Returns int (clean URL query parameter)

Fixes GHSA-vhwj-hfwg-gfg3
Rule: php:S2245 (CWE-338)
@somethingwithproof

Copy link
Copy Markdown
Member

Rebased onto develop to clear the conflict. Your commit and authorship are unchanged; only the CHANGELOG resolution differs.

The conflict was the top line of the develop section, which every open PR here edits. Two of the three entries the branch added were dropped:

  • The eval() entry duplicates security: Remove the eval() calls from the RPN expression evaluator, already on develop.
  • The md5 to sha256 entry describes a change this branch does not make, and develop still has md5(json_encode(...)) at thold_functions.php:7343, so it read as a claim for work that has not happened.

Your own entry further down the file is the one that survived, unedited. The second commit, ci: retry transient dependency downloads, dropped out as already upstream.

Two things left alone because they are yours to decide:

  • The commit replaces rand() with hrtime(true), but GraphCacheBusterTest asserts mt_rand() and the surrounding docblock argues for mt_rand() over random_int(). The test passes either way, so it does not currently exercise the change.
  • hrtime(true) returns nanoseconds since an arbitrary origin, so consecutive page loads can land close together. mt_rand() gives better cache-buster spread if that matters here.

xmacan
xmacan previously approved these changes Sep 15, 2026
These security/robustness fixes were already documented in CHANGELOG.md and
covered by tests (GetAllowedThresholdsTest, TholdCommandExecutionTest,
TholdReplaceThresholdTagsTest, TholdExpressionMathRpnTest,
OptionalCoreFunctionTest), but the corresponding thold_functions.php changes
were missing, leaving the Pest suite failing on every PR built on develop.

- get_allowed_thresholds()/get_allowed_threshold_logs(): bind graph_id and
  caller-supplied params instead of interpolating them into the SQL text,
  and use the *_prepared fetchers.
- thold_replace_threshold_tags(): add a \ flag that quotes
  device/threshold free-text values with cacti_escapeshellarg() when the
  substituted text is a trigger command; email/notification callers are
  unaffected.
- thold_command_execution(): pass shell=true when building the three
  trigger commands, and fix a topic-string typo ('thold' vs 'thold_cmd')
  that silently suppressed exit-status logging for inline (non-queued)
  command runs.
- thold_expression_math_rpn(): remove a stray break that exited the
  operator switch before pushing the 0/0 result, flag modulo-by-zero
  instead of letting PHP throw DivisionByZeroError, reject non-numeric
  unary operands before eval(), and flag SQRT/LOG results that are NAN or
  INF instead of pushing them onto the stack.
@TheWitness
TheWitness requested a review from xmacan September 20, 2026 02:03
thold_functions.php includes includes/arrays.php with a plain include()
(not include_once()) from several of its own functions (e.g. thold_log()),
keyed off \['base_path']. Once any test in the shared Pest process
exercised one of those call sites, PHP's include-once registry considered
the file already included: a later thold_test_load() (require_once) on the
same resolved path silently no-op'd and never (re)published \
to \, depending on which order the test files happened to run in.

This intermittently broke every test that reads \
(TholdReplaceThresholdTagsTest's THOLDTYPE substitution) as well as tests
several call frames downstream of thold_log()/thold_check_threshold()
(NotificationEmailDeduplicationTest, ThresholdTimeBasedCharacterizationTest),
depending on file execution order.

Add thold_test_load_always()/loadPluginSourceAlways(), which use a plain
require() instead of require_once(), for plugin files that only assign
file-scope variables (no function/class declarations) and are therefore
safe to load more than once. Switch the four test classes that load
includes/arrays.php to the new helper.
Adds stderr diagnostics behind THOLD_CI_DEBUG to see the real values
computed on the Linux Pest CI run for the two failures that do not
reproduce locally on Windows. Will be removed once the root cause is
identified.
@TheWitness
TheWitness dismissed somethingwithproof’s stale review September 20, 2026 16:27

Giving copilot the wheel on this one.

xmacan
xmacan previously approved these changes Sep 21, 2026
@TheWitness
TheWitness merged commit 2288c7a into develop Sep 21, 2026
0 of 3 checks passed
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.

5 participants