fix(security): consolidated hardening — SQL injection, XSS, unserialize, trigger_cmd#773
Open
somethingwithproof wants to merge 19 commits into
Open
Conversation
…dening
RLIKE SQL injection: rfilter request variable was concatenated directly into
RLIKE patterns across thold_graph.php (4 instances), thold.php, and
notify_lists.php (3 instances including the notification list filter).
Replaced with db_qstr() which SQL-escapes and quotes the value.
XSS: get_request_var('page') was printed raw into hidden input value
attributes. Wrapped with html_escape().
Unserialize: thold_webapi.php called cacti_unserialize(stripslashes(...))
on POST selected_graphs_array. Replaced with sanitize_unserialize_selected_items()
which validates the result is an array of integers only.
trigger_cmd: thold_set_environ() was called with trigger_cmd_high in both
the low-breach and norm-restoration branches. Corrected to trigger_cmd_low
and trigger_cmd_norm respectively.
No intval() casts added: host_id/site_id go through FILTER_VALIDATE_INT in
the request validation arrays; adding casts after validated request vars is
redundant per Cacti convention.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
25 tests covering RLIKE injection, XSS output escaping, unserialize hardening, and trigger_cmd variable confusion. All tests are source-level static checks — no Cacti bootstrap or database required. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Split the combined OR-match in TriggerCmdRegressionTest into two independent preg_match assertions so a revert of either branch is caught independently. Add FILTER_VALIDATE_IS_REGEX test to RlikeInjectionTest documenting that rfilter goes through regex validation before any RLIKE clause, mitigating ReDoS at the MySQL engine level. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
notify_lists.php: replace array_to_sql_or() and direct $selected_items concatenation with db_execute_prepared() + IN (?,?,?) placeholders for all bulk delete, associate, and disassociate operations. Also replace count() with cacti_sizeof() per Cacti 1.2.x idiom. thold_functions.php: parameterise $graph_id in get_allowed_thresholds() and get_allowed_threshold_logs() using ? placeholders; switch to db_fetch_assoc_prepared() and db_fetch_cell_prepared(). Add PreparedStatementTest.php, Php74CompatibilityTest.php, and Smoke/PhpSyntaxTest.php to cover these patterns. 41 tests pass. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Prevents open redirect via URL manipulation in JS filter forms. Affects thold.php, thold_graph.php, notify_lists.php, notify_queue.php, thold_templates.php, and thold_webapi.php. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
PhpSyntaxTest.php used str_starts_with() (PHP 8.0+) which would fatal under PHP 7.4 before any assertion ran. Use strncmp() instead. Also remove redundant (int) cast and double in_array check on drp_action in notify_lists.php: get_filter_request_var() already validated the value; the second clause was dead and confusing. Replace count() with cacti_sizeof() in setup.php bulk loop. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
array_keys($actions + $assoc_actions) returns integer keys; POST values are always strings. Without the strval() cast, in_array(..., true) with strict comparison always fails, making every bulk form action unreachable. Adds regression test asserting the strval() cast is present. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ndlers All three action-save blocks (save_associate, save_templates, save_tholds) now call get_filter_request_var() for id, notification_action, notification_warning_action, and notification_alert_action before consuming those values via get_request_var() in prepared-statement params. Add inline comment on all RLIKE db_qstr() sites documenting the dual guard: FILTER_VALIDATE_IS_REGEX pre-validates; db_qstr() SQL-escapes. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR hardens the Thold plugin against multiple security issues (SQL injection via RLIKE concatenation and bulk-action concatenation, XSS via unescaped request vars in URLs and HTML, and unsafe deserialization) and corrects a couple of variable-name bugs in thold_command_execution(). It also introduces a Pest-based test suite with stubs/bootstrap for running outside Cacti, plus PHP lint and PHP 7.4 compatibility smoke tests.
Changes:
- Replace raw RLIKE/SQL string concatenation with
db_qstr()and parameterized queries (db_execute_prepared,db_fetch_assoc_prepared,db_fetch_cell_prepared), and add$sql_paramstoget_allowed_thresholds/get_allowed_threshold_logs. - Wrap URL parameters in
encodeURIComponent, escapeget_request_var('page')withhtml_escapein hidden inputs, replacecacti_unserialize(stripslashes(...))withsanitize_unserialize_selected_items, and adddrp_actionwhitelist + per-branch input validation innotify_lists.php. - Fix two
thold_set_environcalls that incorrectly passedtrigger_cmd_highin the low/norm branches; add Pest test suite, bootstrap stubs,composer.json, andphpunit.xml.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| notify_lists.php | Convert bulk SQL to prepared statements; add drp_action whitelist and request var filters; encode URL params |
| thold_functions.php | Add $sql_params, switch to prepared SELECTs; fix trigger_cmd_low/norm environ args; annotate eval/exec with nosemgrep |
| thold_graph.php | Use db_qstr for RLIKE; html_escape page var; encodeURIComponent URL params |
| thold.php | Use db_qstr for RLIKE; encodeURIComponent URL params |
| thold_templates.php | encodeURIComponent on filter |
| thold_webapi.php | Switch to sanitize_unserialize_selected_items; encode URL params |
| notify_queue.php | encodeURIComponent URL params |
| setup.php | Use cacti_sizeof instead of count |
| tests/* | New Pest test suite, bootstrap stubs, smoke/security tests |
| composer.json, phpunit.xml | Test tooling config |
Comments suppressed due to low confidence (1)
notify_lists.php:21
$actionsand$assoc_actionsare not visible in this diff hunk, but the existing code branches further below dispatch ondrp_action == '1'/'2'for at least three different contexts (lists, associate, templates, tholds) which may use overlapping numeric keys. Combining them with$actions + $assoc_actionsonly validates membership in the union of keys, which is fine, but please confirm that all four save_* sections (save_list,save_associate,save_templates,save_tholds) only ever receivedrp_actionvalues present in one of those two arrays — otherwise legitimate actions will hitraise_message(40)and redirect. Ifsave_templates/save_tholdsuse a different action map, they will be rejected here.
| http://www.cacti.net/ |
Verifies that poller_thold.php, setup.php, thold.php, and thold_graph.php contain no single-line raw db_*() calls with interpolated variables. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
get_total_row_data accepts $sql_params since Cacti 1.2.x (lib/auth.php:3120). Both call sites now carry a comment referencing the function signature so future callers understand the API assumption. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
All four form_actions() bulk write blocks now use db_begin_transaction() / db_commit_transaction() so a partial failure cannot leave notification routing state inconsistent across plugin_notification_lists, host, thold_data, and thold_template tables. Adds db_begin/commit/rollback_transaction stubs to test bootstrap and a regression test asserting all four blocks carry transaction guards. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Track $ok across all db_execute_prepared calls in each of the four bulk action blocks. Call db_rollback_transaction() when any statement returns false instead of committing a partial write. Tests: add rollback-count assertion (4), $ok-flag presence check. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…r commit
- Add if (!ok) { break; } at end of each per-item loop in save_associate,
save_templates, and save_tholds so failed iterations halt immediately.
- Move thold_template_update_thresholds calls to after db_commit_transaction()
so the cascade does not participate in the transaction boundary.
- Apply html_escape() to get_filter_request_var('page') output in thold.php
hidden input (mirrors thold_graph.php pattern).
- Add tests: loop-break guard count, template-cascade-after-commit, thold.php
page XSS fix.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This was referenced May 17, 2026
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…notes escapeshellarg PHP_BINARY path in PhpSyntaxTest shell command to handle paths with spaces. Note in thold_functions that thold_set_environ uses putenv side-effects that exec() inherits. Note in bootstrap stub that sanitize_unserialize_selected_items must stay in sync with Cacti core. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…tract Use named variables for the pre-fix vulnerable pattern strings in RlikeInjectionTest to make them easier to verify. Add comments above get_allowed_thresholds/logs noting that sql_params must be supplied when sql_where contains ? placeholders. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Single-quoted string literals with embedded single quotes produced invalid PHP syntax. Nowdoc avoids quoting issues entirely. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…t-circuit delete block PreparedStatementTest regex [^)]+ stopped at first ) inside argument lists and never matched the && $ok chain. Replaced with substr_count. Delete block now chains all eight db_execute_prepared calls with && so the first failure stops execution rather than running all subsequent statements. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Consolidates security hardening from PRs #766, #767, #769, #770, #771, #772 into a single reviewable diff.
SQL injection — bulk form actions (
notify_lists.php)array_to_sql_or()and direct$selected_items[$i]string concatenation withdb_execute_prepared()andIN (?,?,?)placeholders built viaimplode(',', array_fill(0, cacti_sizeof($selected_items), '?')).drp_actionallowlist check usedarray_keys()(returns ints) within_array(..., true)(strict); addedarray_map('strval', ...)to align types.db_begin_transaction()/db_commit_transaction(). Everydb_execute_prepared()return is tracked in$ok; false callsdb_rollback_transaction()and breaks per-item loops immediately.&&so the first failure short-circuits the rest rather than running all statements before rollback.thold_template_update_thresholds()moved after commit so the cascade does not participate in the transaction boundary.SET thold_host_email = 0 AND deleted=""(MySQL parsedAND deleted=""as part of the SET expression, a bug). Corrected:deleted = ""moved to WHERE clause.SQL injection — graph filter (
thold_functions.php)get_allowed_thresholds()andget_allowed_threshold_logs():$graph_idwas interpolated directly into the WHERE clause. Changed togl.id = ?placeholder with$sql_params[]accumulator. Bothdb_fetch_assoc_prepared()anddb_fetch_cell_prepared()now receive bound params.RLIKE injection (
notify_lists.php,thold.php,thold_graph.php)rfiltervalidated viaFILTER_VALIDATE_IS_REGEXbefore reaching any SQL.db_qstr()escapes the already-validated value for use in RLIKE clauses.XSS — hidden inputs and AJAX URL params
get_request_var('page')inthold.phpandthold_graph.phphidden inputs wrapped withhtml_escape().encodeURIComponent()acrossthold.php,thold_graph.php,notify_lists.php,thold_templates.php,thold_webapi.php.Object injection (
thold_webapi.php)sanitize_unserialize_selected_items()applied toselected_graphs_array; rawunserialize()call removed.PHP 7.4 compatibility (
setup.php)count($selected_items)changed tocacti_sizeof($selected_items).Why
All six original PRs addressed overlapping sets of vulnerabilities without complete test coverage or atomic transaction boundaries. Pre-authentication SQL injection via bulk form actions was the highest-severity issue:
array_to_sql_or()constructed WHERE clauses from unsanitized POST data. The RLIKE injection viarfilterand the XSS via thepageparameter were secondary. Atomicity was missing entirely — partial deletes across 8 tables could leave orphaned host references.Test plan
vendor/bin/pest tests/— 53 tests, all passingPreparedStatementTest.php— prepared-statement usage, transaction count, rollback presence,$oktracking, loop break guards, template cascade positionRlikeInjectionTest.php—FILTER_VALIDATE_IS_REGEXpresence,db_qstr()usage, no raw$rfilterinterpolationXssEscapingTest.php—html_escape()on page inputs,encodeURIComponent()in AJAX filtersUnserializeHardeningTest.php—sanitize_unserialize_selected_items()applied, object injection blockedPhp74CompatibilityTest.php— no PHP 8.0+ syntax across 9 plugin filesPreparedStatementConsistencyTest.php— no single-line interpolated rawdb_*()calls in hardened filesPhpSyntaxTest.php(smoke) — all plugin PHP files lint clean