Repository navigation
Harden the NPC controllers and fix review findings - #54
Open
somethingwithproof wants to merge 42 commits into
Open
somethingwithproof wants to merge 42 commits into
somethingwithproof wants to merge 42 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>
…ut creation Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…thod 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>
…art and limit Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…array 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>
Also moves the E2E subprocess runner to support/harness.php and makes the stand-in auth file inert outside the test process. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
… cleanup 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>
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>
… in the front end Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
…nert Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This was referenced Oct 24, 2026
|
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 the problems found in a review of the plugin: security hardening of the request handling and the host import, plus defects that stopped the pages from working at all. This PR contains security-relevant fixes. The security table below lists paths that any user with NPC access could reach.
Two further defects turned up while writing the end-to-end tests and are fixed here too:
npc.phpnever loaded the base controller class (every page ended in a fatal error), and the listing queries used boundLIMITvalues, which MariaDB rejects when Cacti binds them as strings.The change is 41 commits. Each commit is one fix or one group of tests, with a CHANGELOG line where behaviour changed.
Related Issue
No open issue. Review findings are listed below as N-01 to N-22; N-23 and N-24 were found afterwards.
Motivation and Context
On a plain Cacti the NPC pages could not render: the front controller failed on a missing class, and the grids were empty because the JavaScript read a response shape the controllers do not send. The listing queries also failed on the database. Behind that, several actions were open to every NPC user and the host import built a shell command from request values.
Findings and fixes
Security-relevant:
add_device.phpcommand and rejects a non-numeric template id.p_configis ignored, andstartandlimitare bounded (at most 1000 rows).Correctness and reliability:
npc_contacts(it namednpc_contracts) and the deleted-user settings cleanup reads the right column.searchFieldsanddatareach the controllers as JSON; the sanitizer removed their quotes.getSetting,isMapped,mapHost,getSettings,checkHostExists.perfdata.phpandcli.phpskip entries such aslabel=Uinstead of printing warnings into the data input output.npc.phploads the base controller class.LIMITvalues as integers. Cacti passes bound values to PDO as strings and the server rejects a quotedLIMIT.Also found while testing against a real Cacti and fixed here:
addslashes(null)raised the PHP 8.1 deprecation on every page of a fresh install.searchFieldsvalue is treated as empty in the grid search.stripslashes(null)raised the same deprecation.ALTER TABLE.perfdata.phpandcli.phpshare one perfdata parser in the base controller.The translation template is refreshed.
Risk and compatibility
limitis capped at 1000 rows per request.cache/npc_address_cache.json. The old/tmp/npc_address_cache.phpis no longer read and can be deleted. If the cache directory is not writable the cache is rebuilt on each import call.Rollback
Revert the merge commit. There is no data migration to undo. The only artefacts created at runtime are the JSON cache file, which can be deleted.
How Has This Been Tested?
Executed locally with PHP 8.4, Pest through a Docker image with Xdebug, MariaDB 11.8.9 in a throwaway container and Node 22 in Docker. The same CI-order simulation ran against two Cacti trees: a 1.2.x tree (1.2.32) and a develop tree (1.3.0). Each run installed the plugin with
php cli/plugin_manage.php --plugin=npc --install --enable, linted every PHP file, ran Pest with coverage, ran the patch coverage gate, the translation template check, the manifest check, and the workflow'sFATAL:|ERROR:|WARNING:grep overcacti.log.php -l--uninstallstep logs asess_user_idwarning, which the workflow does not runThe two skipped tests are a root-user permission check and the Node wrapper, because the PHP image has no Node. Node ran separately: 34 passed,
js/npc.jsat 100 percent line coverage and 87.6 percent branch coverage. Both cores used the Pest and PHPUnit versions of the develop tree's lock file; I could not run a freshcomposer installfor 1.2.x offline.What the tests cover:
tests/Integration/NpcDatabaseTest.phpruns the schema and the listing, perfdata, cleanup and upgrade queries against MariaDB through PDO with default settings, the way Cacti's database layer sends them. Against the old controllers it fails with theLIMITsyntax error.tests/E2E/RealServerTest.phpstarts PHP's built-in server on the Cacti tree and talks to it with real HTTP, real logins, Cacti's realm checks and CSRF check, and the Cacti database. Four users with different realms (NPC only, no NPC realm, NPC plus commands, NPC plus commands plus devices plus data input) cover: not logged in, wrong realm, wrong method (GET, PUT, DELETE), missing and invalid CSRF token, hostile values, every listing, page bounds, the host import (including the PHP 8 path that used to fatal), bulk comment deletion, data input creation, removed actions, and the command line scripts through Cacti's own bootstrap. The test cutscacti.logback to its length from before the run and skips with a reason when the log is not writable.tests/E2E/FrontControllerTest.phpandCliScriptsTest.phprun the realnpc.php,perfdata.phpandcli.phpwith Cacti's own request helpers.tests/js/npc.test.jsrunsjs/npc.jsunder Node's test runner against a small jQuery and DOM stand-in, loaded withrequire(novmoreval).tests/Unit/JsFrontEndTest.phpruns it from Pest and skips when Node is missing; GitHub's Ubuntu runners have Node.tests/do nothing when requested over the web or run without the test environment; a test runs each one and expects empty output. They use noeval, nohttp://literal and no string-built SQL.Changed-line coverage, measured with Xdebug, merging the in-process Pest run with the line coverage that the subprocess and web server runs record (
tests/E2E/support/coverage-prepend.php, active only when a test run setsNPC_COVERAGE_DIR):controllers/cacti.phpcontrollers/comments.phpcontrollers/controller.phpcontrollers/downtime.phpcontrollers/hosts.phpcontrollers/layout.phpcontrollers/logentries.phpcontrollers/nagios.phpcontrollers/notifications.phpcontrollers/services.phpcontrollers/statehistory.phpcontrollers/sync.phpnagioscmd.phpsetup.phpnpc.phpperfdata.phpcli.phpnpc.php,perfdata.phpandcli.phprun at the top level and cannot be loaded into the unit process, so they are allowlisted in the patch coverage gate and covered by the subprocess and server runs. The line-by-line list of changed lines and the tests that run each is in the traceability file kept with the review notes.Not run locally, left to CI: PHP 8.2 and 8.3; a fresh
composer installof the 1.2.x dependencies; the poller step; the PHPStan and coding standard steps (they run only for non-1.2.x Cacti).Performance
Environment: MariaDB 11.8.9 and PHP 8.4 in Docker on one laptop, Xdebug loaded but coverage mode off. Fixture: 60 hosts, 300 services, 20 hostgroups with each host in 3 (180 memberships), and 100,000 stored checks for one host. Each figure is the median of 7 runs, each run in a fresh PHP process. "Before" is
origin/develop. Before the change the listing queries fail under Cacti's default PDO settings (emulated prepares), so the "before" figures use native prepares, which accept the oldLIMIT ?, ?. That is not how Cacti connects. After figures are given for both modes.The first-run wall times include a cold server cache, so read them as indicative; the row counts are exact. Query counts did not change.
Costs of the change:
SELECTpernpc_settingsrow each time the NPC settings page loads (before, the loop never matched). With 1,000 users that is about 17 ms per page load in this setup.limitclamp add a few array lookups per request. I did not measure them separately.Not measured: opcache effects on the cache include, the front controller as a whole,
perfdata.phpandcli.phpthroughput, and any browser rendering time. I did not use Xdebug cachegrind; none of the changed paths is complex enough to need it.Types of changes
The removal of dispatcher actions and the POST requirement narrow what callers can do; I list them under Risk rather than calling the PR breaking because none of those paths worked or was used by the bundled front end.
Checklist: