Skip to content

Commit f17c7d1

Browse files
hardening: migrate to prepared statements and add input validation
SQL hardening: - Convert all db_execute with $selected_items concatenation to db_execute_prepared - Migrate RLIKE filter values from raw interpolation to db_qstr() - Add intval() guards on integer interpolations in thold.php and thold_graph.php - Replace cacti_unserialize(stripslashes()) with sanitize_unserialize_selected_items() - Fix indentation in notify_lists.php form_actions block - Pin shivammathur/setup-php to full commit SHA in CI Tests and tooling: - Add Pest test framework with PHPStan level 6 - Add XSS output escaping tests - Add prepared statement source-verification tests - Add security audit documentation (SECURITY-AUDIT.md, BACKLOG.md) Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
1 parent c037f27 commit f17c7d1

18 files changed

Lines changed: 6430 additions & 138 deletions

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: CI
2+
on: [push, pull_request]
3+
jobs:
4+
test:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
8+
- uses: shivammathur/setup-php@728c6c6b8cf02c2e48117716a91ee48313958a19 # v2
9+
with: {php-version: '8.4', coverage: xdebug}
10+
- run: composer install --no-interaction
11+
- run: vendor/bin/pest --coverage
12+
- run: vendor/bin/phpstan analyse --memory-limit=512M

BACKLOG.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Backlog: plugin_thold
2+
3+
Issues derived from SECURITY-AUDIT.md findings and tooling setup.
4+
5+
---
6+
7+
### Issue #1: hardening(sql): add allowlist guard for column name interpolation in setup.php
8+
9+
- **Priority**: high
10+
- **Labels**: [security, hardening, priority: high]
11+
- **Branch**: hardening/1-thold-column-name-allowlist
12+
- **Evidence**: setup.php:345,350,386,391
13+
- **Acceptance criteria**:
14+
- [ ] A constant or function defines the exact set of allowed column suffixes: `['hi', 'low', 'warning_hi', 'warning_low']`
15+
- [ ] Each call site checks `in_array($matches[1], $allowed, true)` before interpolation
16+
- [ ] An unexpected value logs a warning via `cacti_log()` and skips the replacement
17+
- [ ] Existing behaviour is unchanged for valid suffixes
18+
- [ ] Unit test in `tests/Security/XssOutputTest.php` passes (allowlist describe block)
19+
- **Dependencies**: none
20+
21+
---
22+
23+
### Issue #2: hardening(sql): replace concatenated DELETE/UPDATE with prepared statements in notify_lists.php
24+
25+
- **Priority**: high
26+
- **Labels**: [security, hardening, priority: high, tech-debt]
27+
- **Branch**: hardening/2-notify-lists-prepared-statements
28+
- **Evidence**: notify_lists.php:484,488
29+
- **Acceptance criteria**:
30+
- [ ] `db_execute('DELETE ... thold_id=' . $selected_items[$i])` replaced with `db_execute_prepared(..., [(int) $selected_items[$i]])`
31+
- [ ] `db_execute('UPDATE thold_data SET notify_alert=' . ...)` replaced with prepared equivalent
32+
- [ ] All other concatenated queries in notify_lists.php audited and converted
33+
- [ ] Existing test suite passes
34+
- **Dependencies**: none
35+
36+
---
37+
38+
### Issue #3: hardening(auth): prevent direct HTTP access to thold_webapi.php
39+
40+
- **Priority**: high
41+
- **Labels**: [security, hardening, priority: high]
42+
- **Branch**: hardening/3-thold-webapi-context-guard
43+
- **Evidence**: thold_webapi.php:1 (missing context guard)
44+
- **Acceptance criteria**:
45+
- [ ] A `defined('IN_CACTI_CONTEXT') or die(...)` guard is added at the top of thold_webapi.php
46+
- [ ] `thold.php` defines `IN_CACTI_CONTEXT` before including thold_webapi.php
47+
- [ ] Direct HTTP GET to thold_webapi.php returns a non-200 response or no output
48+
- [ ] Existing functionality via thold.php is unaffected
49+
- **Dependencies**: none
50+
51+
---
52+
53+
### Issue #4: hardening(csrf): add form token verification to state-changing POST handlers
54+
55+
- **Priority**: medium
56+
- **Labels**: [security, hardening, priority: medium]
57+
- **Branch**: hardening/4-thold-csrf-tokens
58+
- **Evidence**: notify_lists.php:537, thold.php:386, thold_templates.php:241
59+
- **Acceptance criteria**:
60+
- [ ] `form_verify_token()` (or Cacti equivalent) called before each bulk-action handler
61+
- [ ] Invalid token results in HTTP 400 and no state change
62+
- [ ] CSRF test in `tests/Security/XssOutputTest.php` passes (after seam is created)
63+
- **Dependencies**: Issue #5 (seam)
64+
65+
---
66+
67+
### Issue #5: tech-debt(seam): extract bulk-action DB calls to src/ for testability
68+
69+
- **Priority**: medium
70+
- **Labels**: [tech-debt, testability]
71+
- **Branch**: tech-debt/5-thold-src-seams
72+
- **Evidence**: notify_lists.php:484, setup.php:345, thold_webapi.php action handlers
73+
- **Acceptance criteria**:
74+
- [ ] `src/TholdColumnResolver.php` provides `resolve(string $suffix): string` with allowlist
75+
- [ ] `src/NotificationListRepository.php` provides typed methods for delete/update operations
76+
- [ ] `src/WebApiDispatcher.php` provides auth-gated action dispatch
77+
- [ ] All existing behaviour preserved
78+
- [ ] PHPStan level 6 passes on `src/`
79+
- **Dependencies**: none
80+
81+
---
82+
83+
### Issue #6: tooling(ci): configure Composer, Pest 4, PHPStan, and Infection for plugin_thold
84+
85+
- **Priority**: medium
86+
- **Labels**: [tooling, ci]
87+
- **Branch**: tooling/6-thold-composer-ci
88+
- **Evidence**: composer.json, phpstan.neon, infection.json, pest.php created in this audit
89+
- **Acceptance criteria**:
90+
- [ ] `composer install` succeeds
91+
- [ ] `vendor/bin/pest` runs and reports test results
92+
- [ ] `vendor/bin/phpstan analyse` runs at level 6 on `src/` and `tests/`
93+
- [ ] CI workflow runs on push and pull_request
94+
- [ ] Coverage threshold 80% enforced in CI
95+
- **Dependencies**: Issue #5 (src/ must have testable code)

SECURITY-AUDIT.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Security Audit: plugin_thold
2+
3+
Audit date: 2026-03-09
4+
Auditor: static analysis (grep + manual code review)
5+
Cacti environment: not available; findings based on source only.
6+
7+
---
8+
9+
## Findings
10+
11+
### FIND-001: [SQL] Column name interpolation in setup.php
12+
13+
- **Severity**: Medium
14+
- **Confidence**: High
15+
- **File**: setup.php
16+
- **Line**: 345, 350, 386, 391
17+
- **Evidence**:
18+
```
19+
345: db_fetch_cell_prepared('SELECT thold_' . $matches[1] . ' FROM thold_data WHERE data_template_rrd_id = ?', [$data_template_rrd_id])
20+
350: db_fetch_cell_prepared('SELECT time_' . $matches[1] . ' FROM thold_data WHERE data_template_rrd_id = ?', [$data_template_rrd_id])
21+
386: db_fetch_cell_prepared('SELECT thold_' . $matches[1] . ' FROM thold_data WHERE data_template_rrd_id = ?', [$data_template_rrd_id])
22+
391: db_fetch_cell_prepared('SELECT time_' . $matches[1] . ' FROM thold_data WHERE data_template_rrd_id = ?', [$data_template_rrd_id])
23+
```
24+
- **Description**: `$matches[1]` is interpolated directly into the column name portion of the query. The value comes from two regex captures: `/\|thold\:(hi|low)\:/` (captures `hi` or `low`) and `/\|thold\:(warning_hi|warning_low)\:/` (captures `warning_hi` or `warning_low`). PDO prepared statements cannot parameterise column names, so if the regex capture were ever widened or reused elsewhere with a different pattern, SQL injection becomes possible.
25+
- **Exploitability**: Low in current form — the regex strictly constrains captures to a four-value set. Risk is that the pattern is copy-pasted or the regex is relaxed without updating the column-name guard.
26+
- **Remediation**: Replace interpolation with an explicit allowlist check:
27+
```php
28+
$allowed_thold = ['hi', 'low', 'warning_hi', 'warning_low'];
29+
if (!in_array($matches[1], $allowed_thold, true)) {
30+
cacti_log('WARNING: unexpected thold column suffix: ' . $matches[1], false, 'THOLD');
31+
break;
32+
}
33+
$value = db_fetch_cell_prepared('SELECT thold_' . $matches[1] . ' FROM thold_data WHERE data_template_rrd_id = ?', [$data_template_rrd_id]);
34+
```
35+
- **TDD status**: Ready (tests in tests/Security/XssOutputTest.php, allowlist describe block)
36+
37+
---
38+
39+
### FIND-002: [SQL] Unparameterised DELETE in notify_lists.php
40+
41+
- **Severity**: Low
42+
- **Confidence**: High
43+
- **File**: notify_lists.php
44+
- **Line**: 484, 488
45+
- **Evidence**:
46+
```
47+
484: db_execute('DELETE FROM plugin_thold_threshold_contact WHERE thold_id=' . $selected_items[$i]);
48+
488: db_execute('UPDATE thold_data SET notify_alert=' . get_request_var('id') . ' WHERE id=' . $selected_items[$i]);
49+
```
50+
- **Description**: Integer values are concatenated rather than bound. `$selected_items` comes from `sanitize_unserialize_selected_items()` which validates each element with `input_validate_input_number()`, so the current exploitability is low. The pattern is inconsistent with the rest of the codebase which uses prepared statements.
51+
- **Exploitability**: Low — requires bypass of `sanitize_unserialize_selected_items()` integer validation.
52+
- **Remediation**:
53+
```php
54+
db_execute_prepared('DELETE FROM plugin_thold_threshold_contact WHERE thold_id = ?', [(int) $selected_items[$i]]);
55+
db_execute_prepared('UPDATE thold_data SET notify_alert = ? WHERE id = ?', [(int) get_filter_request_var('id'), (int) $selected_items[$i]]);
56+
```
57+
- **TDD status**: Ready
58+
59+
---
60+
61+
### FIND-003: [Auth] thold_webapi.php — review endpoint auth coverage
62+
63+
- **Severity**: Medium
64+
- **Confidence**: Medium
65+
- **File**: thold_webapi.php
66+
- **Line**: 1 (file entry), all action handlers
67+
- **Evidence**: File is included via `thold.php:39: include_once(...thold_webapi.php)`. Auth is inherited from `thold.php` which includes `auth.php`. Direct HTTP access to thold_webapi.php is not blocked since it lacks `cli_check.php` or a standalone auth header.
68+
- **Description**: If thold_webapi.php can be accessed directly (not via thold.php), its action handlers run without the auth check applied by thold.php's include chain.
69+
- **Exploitability**: Medium — requires direct URL access to thold_webapi.php. Cacti's web server configuration may mitigate this if the plugins directory requires auth at the vhost level.
70+
- **Remediation**: Add at the top of thold_webapi.php (or confirm it is never directly web-accessible):
71+
```php
72+
if (!defined('IN_CACTI_CONTEXT')) {
73+
die('<br><strong>This file is not meant to be accessed directly.</strong>');
74+
}
75+
```
76+
- **TDD status**: Seam required first
77+
78+
---
79+
80+
### FIND-004: [CSRF] State-changing POST handlers lack explicit token check
81+
82+
- **Severity**: Medium
83+
- **Confidence**: Medium
84+
- **File**: notify_lists.php, thold.php, thold_templates.php
85+
- **Line**: notify_lists.php:537, thold.php:386, thold_templates.php:241
86+
- **Evidence**:
87+
```
88+
notify_lists.php:537: foreach ($_POST as $var => $val) { if (preg_match('/^chk_([0-9]+)$/', ...
89+
thold.php:386: foreach ($_POST as $var => $val) {
90+
```
91+
- **Description**: Mass-action POST handlers (bulk delete, bulk associate) do not call an explicit CSRF token verification function. Cacti core provides `form_verify_token()` / `check_request_token()` — these plugins do not call it before processing state-changing actions.
92+
- **Exploitability**: Medium — requires a logged-in admin to visit an attacker-controlled page. Cacti's SameSite cookie settings may mitigate in modern browsers.
93+
- **Remediation**: Add token check at the top of each action handler:
94+
```php
95+
if (!form_verify_token()) {
96+
header('HTTP/1.1 400 Bad Request');
97+
exit;
98+
}
99+
```
100+
- **TDD status**: Seam required first (need to stub `form_verify_token`)
101+
102+
---
103+
104+
## Unknowns
105+
106+
- Cannot confirm whether Cacti's auth layer enforces session checks before any plugin file is served — no live Cacti environment available.
107+
- `thold_daemon.php` uses `exec_background()` which calls PHP's `exec`; could not confirm whether any user-controlled data reaches `$process` arguments without full call-graph tracing.
108+
109+
## Blind Spots
110+
111+
- No dynamic analysis performed. Race conditions in threshold evaluation not assessed.
112+
- Email/notification template injection (thold_functions.php notification rendering) not fully traced.
113+
- No review of JavaScript files in themes/ for DOM-based XSS.
114+
115+
## Assumptions
116+
117+
- Cacti's `sanitize_unserialize_selected_items()` correctly validates all elements as integers.
118+
- `auth.php` include at thold.php top correctly redirects unauthenticated requests.
119+
- `db_qstr()` used in notify_queue.php filter queries provides adequate escaping.
120+
121+
## Seams Needed
122+
123+
1. Extract column-name resolution (FIND-001) to `src/TholdColumnResolver.php`.
124+
2. Extract bulk-action DB calls (FIND-002) to `src/NotificationListRepository.php`.
125+
3. Extract webapi action dispatch (FIND-003) to `src/WebApiDispatcher.php` with auth gate.
126+
4. Stub `form_verify_token` in CactiStubs.php to enable CSRF tests (FIND-004).

SECURITY.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Security Policy
2+
3+
## Supported Versions
4+
5+
This plugin follows the Cacti project's support policy. Security fixes are
6+
applied to the current development branch and backported per project policy.
7+
8+
## Reporting a Vulnerability
9+
10+
Report security vulnerabilities via the Cacti project's private security
11+
disclosure process:
12+
13+
- GitHub Security Advisories: https://github.com/Cacti/plugin_thold/security/advisories
14+
- Do NOT open public issues for security vulnerabilities.
15+
16+
Please include:
17+
- Description of the vulnerability
18+
- Steps to reproduce
19+
- Affected versions
20+
- Suggested remediation (if known)
21+
22+
A maintainer will acknowledge the report within 72 hours and provide a
23+
remediation timeline.
24+
25+
## Security Hardening Notes
26+
27+
See SECURITY-AUDIT.md for the current known finding backlog and remediation status.

composer.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "cacti/plugin_thold",
3+
"description": "Cacti plugin_thold plugin",
4+
"type": "cacti-plugin",
5+
"require": {"php": ">=8.4"},
6+
"require-dev": {
7+
"pestphp/pest": "^4.0",
8+
"phpstan/phpstan": "^2.0",
9+
"phpstan/phpstan-strict-rules": "^2.0",
10+
"ergebnis/phpstan-rules": "^2.0",
11+
"infection/infection": "^0.29",
12+
"rector/rector": "^2.0",
13+
"mockery/mockery": "^1.6"
14+
},
15+
"autoload": {"psr-4": {"Cacti\\PluginThold\\": "src/"}},
16+
"autoload-dev": {"psr-4": {"Cacti\\PluginThold\\Tests\\": "tests/"}},
17+
"scripts": {
18+
"test": "vendor/bin/pest",
19+
"analyse": "vendor/bin/phpstan analyse --memory-limit=512M",
20+
"mutate": "vendor/bin/infection --threads=4"
21+
},
22+
"config": {"allow-plugins": {"pestphp/pest-plugin": true, "infection/extension-installer": true}}
23+
}

0 commit comments

Comments
 (0)