Skip to content

security: replace eval() in RPN binary math with safe dispatch (GHSA-vr4v-qvqm-gm9j) - #807

Merged
TheWitness merged 11 commits into
developfrom
advisory-fix-eval-math
Sep 21, 2026
Merged

TheWitness merged 11 commits into
developfrom
advisory-fix-eval-math

Conversation

@bmfmancini

Copy link
Copy Markdown
Member

Security Fix — GHSA-vr4v-qvqm-gm9j

Fixes GHSA-vr4v-qvqm-gm9j

Summary

The thold_expression_math_rpn() function in thold_functions.php used PHP's eval() to evaluate binary arithmetic operations (line 378) during RPN expression parsing for threshold expressions.

Vulnerability Details

  • Rule: php:S1523 (SonarQube)
  • CWE: CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code)
  • OWASP: A03:2021 - Injection
  • Severity: Critical
  • Location: thold_functions.php, line 378

Changes

Replaced eval() with a safe arithmetic dispatch function thold_rpn_math_binary() that uses a switch statement to perform binary math operations (+, -, *, /, %) without dynamic code execution.

Testing

  • PHP syntax check passes (php -l thold_functions.php)
  • All binary math operators covered: +, -, *, /, %
  • Division-by-zero protection preserved

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.

This needs isolation and behavioral coverage before merge. #773 already replaces the same binary eval block, so merging both independently risks one implementation stomping the other. Rebase on the selected implementation, retain one safe dispatch, and add Cacti-Composer/Pest cases for operand order, + - * / % ^, 0/0, divide-by-zero, modulo-by-zero, and an invalid operator. The current implementation's default return of 0 silently masks an impossible operator, and all checks are red.

…1523)

Replace the eval()-based binary arithmetic evaluation at line 378
with a new thold_rpn_math_binary() helper function that uses a
switch statement for +, -, *, /, %, ^ operators.

The operator was already whitelisted via in_array(, , true)
and operands validated with is_numeric(), but removing eval() eliminates
the code injection risk entirely (CWE-95).

Addresses SonarQube finding: php:S1523 at thold_functions.php:378
@somethingwithproof

Copy link
Copy Markdown
Member

The focused tests and green CI are useful progress. Two blockers remain: #773 already changes this exact evaluator, so this must be rebased onto whichever implementation is selected; and the invalid/default path still returns numeric 0, converting an evaluator error into a plausible threshold value. Please make that path unambiguously fail closed and add divide-by-zero/modulo-by-zero coverage. Also split the shared workflow retry/matrix edits into one dedicated PR instead of repeating them across each security patch.

xmacan
xmacan previously approved these changes Sep 15, 2026
Same set of pre-existing gaps as develop (covered by tests but never
implemented in thold_functions.php), plus one bug specific to this PR:

- 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.
- thold_command_execution(): pass shell=true for the three trigger
  commands, and fix a topic-string typo ('thold' vs 'thold_cmd') that
  silently suppressed exit-status logging for inline 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 (unary math here is still
  eval()-based; unrelated to this PR's binary-math dispatcher).
- thold_rpn_math_binary(): this PR's new dispatcher mapped '^' to pow(),
  silently changing it from the pre-existing bitwise XOR behaviour
  (eval('... ^ ...') always computed XOR). Restored XOR to avoid breaking
  existing user thresholds, and corrected this PR's own
  TholdRpnBinaryMathTest 'power' dataset, which had asserted the
  incorrect pow() behaviour.
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.
cigamit
cigamit previously approved these changes Sep 20, 2026
…math

# Conflicts:
#	CHANGELOG.md
#	thold_functions.php
Merging develop brought in #796's fix for thold_check_threshold()'s type-2
(time-based) restoral logic (`<` -> `>=` for the warning/alert fail-count
comparisons). This test harness always stubs the log-derived fail count
to 0, and the two restoral tests here relied on the default trigger of 1,
so under the corrected operator (0 >= 1 is false) neither restoral branch
fired anymore and both tests broke.

Override time_fail_trigger/time_warning_fail_trigger to 0 in both tests,
representing a threshold with no minimum streak requirement, so the
harness's stubbed zero count still satisfies the corrected `>=` check and
the tests exercise real restoral behavior again instead of the inverted
bug.
@TheWitness
TheWitness requested a review from cigamit September 20, 2026 16:26
@TheWitness
TheWitness merged commit aba15fd into develop Sep 21, 2026
4 of 5 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.

6 participants