Repository navigation
Harden the Host MIB pages and poller, and fix review findings - #77
Open
somethingwithproof wants to merge 50 commits into
Open
somethingwithproof wants to merge 50 commits into
somethingwithproof wants to merge 50 commits into
Conversation
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…raph Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…lected Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…able Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…odes Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…ert on the web Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…writable log, and share the suite setup Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
… cacti.log after each test Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Merged
2 of 6 tasks
A timed out walk wiped the host's rows for that table. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
A run past the 600 s purge could overlap the next one, and a stuck collector made it wait forever. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Also stop the scan once the run time is used up. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…story Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…tabs The Device list now binds the chosen OS type. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…vices tabs Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…r queries (stacked on #77) (#79) * fix: declare the graph filter helper Cacti 1.2.x leaves out of plugin pages Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix: classify unclassified devices with one UPDATE per stored type Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
TheWitness
requested changes
Oct 24, 2026
| form_selectable_cell('<a class="linkEditMain" href="' . htmlspecialchars('hmib_types.php?action=edit&id=' . $host_type['id']) . '">' . $host_type['name'] . '</a>', $host_type['id']); | ||
| form_selectable_cell($host_type['version'], $host_type['id']); | ||
| form_selectable_cell('<a class="linkEditMain" href="' . htmlspecialchars('hmib_types.php?action=edit&id=' . $host_type['id']) . '">' . html_escape($host_type['name']) . '</a>', $host_type['id']); | ||
| form_selectable_cell(html_escape($host_type['version']), $host_type['id']); |
Member
Author
There was a problem hiding this comment.
I could not tell what change these three comments ask for. The cells already go through html_escape(). Could you say what should change here?
| form_selectable_cell($host_type['totals'], $host_type['id']); | ||
| form_selectable_cell($host_type['sysObjectID'], $host_type['id']); | ||
| form_selectable_cell($host_type['sysDescrMatch'], $host_type['id']); | ||
| form_selectable_cell(html_escape($host_type['sysObjectID']), $host_type['id']); |
| form_selectable_cell($host_type['sysObjectID'], $host_type['id']); | ||
| form_selectable_cell($host_type['sysDescrMatch'], $host_type['id']); | ||
| form_selectable_cell(html_escape($host_type['sysObjectID']), $host_type['id']); | ||
| form_selectable_cell(html_escape($host_type['sysDescrMatch']), $host_type['id']); |
Auto purge no longer deletes Use History rows; the new setting defaults to keeping them. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Each stored column and direction is checked against the table's sortable columns. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…pty table An empty walk is trusted only when the device still answers a sysUpTime GET. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
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.



Description
Fixes thirteen defects found in a read-through of the plugin and by the new end-to-end tests, several of them security relevant: two SQL injection paths, one stored cross-site scripting path, a request-forgery path, and an unvalidated ORDER BY. Each fix is its own commit with a regression test. The remaining changes make the test suite able to run the plugin's SQL against MariaDB.
This pull request contains security-relevant fixes. Details are in the table below; none of them is exploitable without either a Host MIB login or control of a monitored device's SNMP replies.
Related Issue
Relates to #41 (defense-in-depth audit) and #39 (SQL hardening). It does not close either; #41 lists items that this change does not touch.
Motivation and Context
hmib_get_graph_url()rescan_types()ORbranch was also not limited to unclassified devices, so typed devices were reassigned, and the RLIKE branch contradicted the form text that says regular expressions were removed.host_type = 0, drop RLIKE.html_escape()each value.?scan=trueran the rescan from a GET link, which Cacti's CSRF protection does not cover.$host_id != ''with an int: on PHP 8,0 != ''is true, so "no device" becamegl.host_id=0and the data-query graph links on the Summary tab disappeared.!= 0(the existing-1sentinel still restricts).AND (, but had no WHERE clause, so any filter gave a SQL syntax error.action=running%action=running, which decodes to an unknown action and shows an empty page.INSERT IGNORE ... SELECT DISTINCT,LEFT(..., 100), skip empty values, bound names.add_device.php.hrSWRun_last_seen.total_timeupsert added the performance interval on every poller cycle, not once per performance collection.collectHostIndexedOid()present=0before walking and deleted the unmarked ones after. Core returns an empty array for a timed out walk, so one timeout deleted the device's processes, storage, hardware, software or processor rows.$snmp_errorempty), so an empty table is trusted only when the device then answers a sysUpTime GET, which core does report as failed; then the stale rows are cleared. Mark and delete with bound values.plugin_hmib_processesrows, purged after 600 s, so a run longer than that let the next one start beside it. The wait loop had no deadline, and the launch placeholder key wasrand(), which can equal a real process id.GET_LOCK()for the main run (the server drops it with the connection; released again at shutdown). Stop launching and waiting 600 s after the run began. Placeholder keys start at 2^31. Seed and key are bound.autoDiscoverHosts()$add = true; if ($add)was dead code.<label for>.hmib_summary_order_by()get_order_string()dropped shift-click multi-column sorting, which 1.2.x core keeps in$_SESSION['sort_data'].Not changed: the union return types in setup.php and hmib.php (
array|string,string|int). The repository's CI matrix andPhpCompatibilityTestset the floor at PHP 8.2, so they are valid. Raising or lowering the floor is a separate decision.Behavior changes to be aware of:
hmib_types.php?scan=truedirectly must POST.=,+,-or@gain a leading single quote.How Has This Been Tested?
Every fix has a test that fails on the previous code. With the four production files restored to origin/develop, the suite reports 97 failures; with this branch it reports none.
The tests are of four kinds:
tests/Unit,tests/Security). The page scripts and the pollers chdir, include Cacti's bootstrap or exit at the top level, so a test cannot include them. These tests cut one function out of the source text and run it (hmib_test_define_function()intests/bootstrap-unit.php, which writes the function to a private temporary file, requires it and deletes it).tests/Integration/HmibSqlDbTest.php). The plugin's own statements run against a real Cacti schema in MariaDB, each in three SQL modes: strict, not strict, and strict with ONLY_FULL_GROUP_BY.tests/E2E). They start PHP's built-in web server on the Cacti install the plugin sits in, log in as three users created for the test (viewer and admin realms, viewer only, and no plugin realm), and send real requests that are answered from the install's database. The pollers andassociate_os_type.phprun as subprocesses. 140 of the 235 tests are end-to-end. Paths covered: the Summary tab with and without a filter, for every sortable column in both directions, with hostile sort values, with 1,200 processes, with quotes in process names and an empty database; the Devices, Processes and Graphs tabs and the links on each device row, followed through to the page they open; the OS type page with markup in every stored field, the rescan by POST and its rejection by GET, by missing token, by lacking realm and when anonymous, CSV export with quote and formula characters, import with and without update, with quotes, with markup, with a bad header and with no file, and the edit form; graph automation with only the users template, only the processes template, both, neither, and the summary device created from its template; the per-device worker with and without the performance collector due; the main run; and OS type association.TestsDirectoryInertTestshows that every PHP helper undertests/answers 404 with no body when a web server executes it, and that the coverage helper does nothing unless the test process asks for it, writes only inside the directory it is given, and sanitises the test name it uses in a file name.The end-to-end tests skip when the install, its database or the plugin is missing. The CI workflow starts MariaDB, installs Cacti and installs the plugin before the Pest step, so they run there; the connection settings come from the install's
include/config.php.To see which test runs a changed line,
tests/support/coverage.phpandtests/bin/coverage-prepend.phprecord Xdebug coverage test by test (in the test process and in the server and subprocesses it starts), andtests/bin/merge-coverage.phpmerges the dumps and lists, for each changed executable line, the tests that ran it. All 52 changed executable lines of the four production files are run (26 in hmib.php, 19 in hmib_types.php, 3 in poller_graphs.php, 4 in poller_hmib.php), by between 2 and 19 tests each. The last commit makes CI run that second, per-test pass and feed the merged result totests/bin/patch-coverage.php, so the four files no longer need the script's allowlist: it is now empty, andtests/bin/patch-coverage.phpreports 100.00% (52/52 lines) against origin/develop.Results, all run locally in Docker (PHP 8.4.27 with Xdebug 3, MariaDB 11.8.9), in the CI order: database loaded from the core's
cacti.sql,php cli/plugin_manage.php --plugin=hmib --install --enable, Pest with Clover, Pest per test, merge, patch coverage:The skips are a permissions test that cannot fail as root, the SNMP-agent test (no agent answers in the container; with snmpd on the CI runner it checks that a second worker run leaves the time seen total alone), and, on 1.2.x only, the Graphs tab test, because that core defines
graph_view_has_untemplated_graphs()ingraph_view.phponly and so its graph filter fatals on a plugin page. That is a defect of the Graphs tab on 1.2.x that exists before this change; it is not fixed here.Also run:
php -lon every PHP file,tests/bin/validate-manifest.php,tests/bin/check-i18n-pot.php origin/develop. The 1.2.x run used the core's own tree with the Pest from the develop tree, because Composer could not install the core's dev dependencies offline.Not run locally: PHP 8.2 and 8.3 (the CI matrix), the SNMP-agent test, the CI workflow itself (the added step was run by hand in the same order), the Cacti installer (the databases were loaded from
cacti.sql), and any browser. Xdebug's branch data is reported by the merge tool but not relied on: it marks three branches of changed functions as not taken although the lines inside them ran.Rules the new test code follows: no eval, no weak hashes, no URL or IP literals apart from the loopback address of a silent UDP socket in one poller test, no SQL built from variables (the table copy-and-restore statements are spelled out), the test users' passwords are random and hashed with
password_hash(), and nothing deletes outside a directory the test created.tests/bootstrap-unit.phpand the other helpers undertests/exit with a 404 when requested over the web, becausetests/can sit in the web root on git-clone installs.The H rows were added after the review. Their tests fail on the code before each fix. Run locally in Docker in the CI order (Cacti 1.2.x, MariaDB 11.8.9, the Cacti log made writable as the workflow now does) on PHP 8.2, 8.3 and 8.4: PHP 8.2.35, 8.3.36 and 8.4.27 each ran 301 tests, 300 passed and 1 skipped, in both Pest passes; patch coverage 100.00% (256/256 lines); the translation template check, the poller smoke step and the log check pass. The new workflow step runs
poller_hmib.php -M --forceas www-data after the Cacti poller, checks that it finishes and that the plugin tables exist; it first waits for the main run the Cacti poller starts in the background, which the run lock would otherwise refuse.Performance
Measured with a throwaway harness (not part of the pull request) that runs the functions cut from origin/develop and from this branch against the same MariaDB 11.8.9 container. The harness uses PDO directly, not Cacti's database layer. Median of 9 runs (5 for the rescan); wall time from
hrtime, CPU fromgetrusage, queries fromSHOW SESSION STATUS LIKE 'Questions'. Fixture:graph_local20,000 rows over 2,000 devices,plugin_hmib_hrSWRun50,000 rows (500 distinct names),plugin_hmib_hrSystem200 unclassified devices with distinct sysDescr, 20 stored types.rescan_types(), 200 unclassified devices, 20 typesReading the numbers: the only cost is in F-1. Restoring the intended "all devices" link makes the link carry every matching graph id (about 5 ms and 8 MB here for 20,000 graphs). The 50-link loop differences are within run-to-run noise. The rescan keeps its existing shape of one UPDATE per unclassified device type and stored type pair (4,006 queries here), so a large unclassified fleet with many stored types is slow before and after; this change does not alter that. The count query, the sort allowlist, the CSV helper, the escaping and the
total_timechange add no queries. F-12 adds one query, once, when the summary device is created.The Summary sort defaults (F-14) add two array lookups with defaults per request and no query. CI cost, measured locally: the full suite takes about 36 s in the first Pest step (with Clover) and 110 to 185 s in the new per-test pass, because every request the server answers is instrumented in that pass; that is the price of the second pass on pull requests.
Not measured: Cacti's own
db_*wrappers, rendered page time, the poller with real SNMP devices, memory growth in the poller, and Xdebug cachegrind.Screenshots (if appropriate):
None.
Types of changes
The rescan now requires POST, which breaks a script that called the old GET link.
Rollback
Each commit is independent apart from the shared test helpers added in
tests/bootstrap-unit.php. Reverting one commit restores that behavior only. The last commit changes the CI workflow and empties the allowlist intests/bin/patch-coverage.php; revert it alone to measure only the files the unit run loads again. No schema, INFO or install changes are involved, so nothing needs a data migration. Rows added by the rescan are ordinaryplugin_hmib_hrSystemTypesrows and remain valid after a revert.Checklist: