fix(security): consolidated hardening — SQL injection, XSS, unserialize, trigger_cmd#773
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>
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>
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>
…tags RPN math evaluator: replace the two eval() sinks in thold_expression_math_rpn() with native switch dispatch. The binary operator case now computes results directly (+ 0 coercion and (int) casts preserve eval()'s integer semantics for % and ^); the unary-function case gains a missing is_numeric($v1) guard before dispatching to sin()/cos()/sqrt()/etc. Operands were already validated numeric and operators/functions were already whitelisted, so behavior is unchanged for legitimate RPN expressions while the code-execution sink is eliminated. trigger_cmd execution: thold_replace_threshold_tags() gains a $shell flag. When set (only the three trigger_cmd_high/low/norm exec() callers pass true), device/user-derived string tags (<DESCRIPTION>, <HOSTNAME>, <LOCATION>, <SITE>, <THRESHOLDNAME>, <DSNAME>, <NOTES>, <DNOTES>, <DEVICENOTE>, <EXTERNALID>) are escaped with cacti_escapeshellarg() so they cannot break out of the admin-configured command line. Email/HTML callers keep the default $shell=false and are unaffected. Numeric/computed tags remain unescaped.
|
@TheWitness @netniV — requesting a final review on this consolidated security hardening PR when you have a moment. Latest push (
Validation: Known follow-ups:
|
… env comments) notify_lists.php: convert the two notify_warning_extra/notify_extra clearing queries from double-quoted PHP strings (with escaped double quotes) to single-quoted strings, matching the surrounding prepared queries in the same block. The rendered SQL is byte-identical (verified), so this is purely a readability/consistency change that removes the escape-character confusion the review flagged. thold_functions.php: mirror the putenv() side-effect comment onto the trigger_cmd_high and trigger_cmd_norm branches (it previously existed only on trigger_cmd_low) so all three exec() paths document that thold_set_environ() populates the environment exec() inherits.
|
Thanks for the automated review. Status of each point below — most were already handled in earlier commits on this branch; the two outstanding items are addressed in Addressed in
Already handled earlier on this branch:
No change (with rationale):
|
|
@TheWitness @netniV — this is ready for a final review when you have a moment. Status
CI heads-up
Before merge
Thanks! |
Integrates Cacti#774 (RPN evaluation / SNMP trap / notification bug fixes). The thold_functions.php conflict is resolved by keeping this PR's eval()-free native-switch RPN evaluator on top of Cacti#774's operand guards and division-by-zero handling; queued exec() nosemgrep annotations aligned to develop. No eval() sinks remain; php -l clean.
Update: merged latest
|
Keep this branch workflow-only. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
sanitize_unserialize_selected_items rejects nested wizard arrays and broke thold_new_graphs_save. Use the graphs sanitizer when available, else allowed_classes => false with structure checks. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
… PR" This reverts commit 21a03b3.
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