From c955b75ad792682da28e7fb5c9c08be6a82f95ec Mon Sep 17 00:00:00 2001 From: TheWitness Date: Wed, 7 Oct 2026 16:55:07 -0400 Subject: [PATCH 1/2] security: convert cactiReturnTo cancel buttons to the CSP-safe class The confirmation-page Cancel buttons on webseer.php, webseer_servers.php and webseer_proxies.php used an inline onClick='cactiReturnTo()' handler, which trips Cacti's Content-Security-Policy script-src-attr directive. Switch them to the CSP-safe cactiReturnTo CSS class (class='cactiReturnTo', plus an optional data-url when a target is passed). Cacti 1.2.31 and later bind this class automatically. To keep the plugin's 1.2.29 compatibility floor, the README documents a one-time include/layout.js snippet for operators still on 1.2.29-1.2.30 - no shim is baked into the core or the plugin. --- CHANGELOG.md | 1 + README.md | 14 ++++++++++++++ webseer.php | 8 ++++---- webseer_proxies.php | 2 +- webseer_servers.php | 6 +++--- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c0e770..f812827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ --- 3.3 --- +* security: Replace the confirmation-page Cancel buttons' inline `onClick='cactiReturnTo()'` with the CSP-safe `cactiReturnTo` class so the pages no longer trip Cacti's Content-Security-Policy script-src-attr directive; see the README for the one-time `applySkin()` snippet needed when running Cacti below 1.2.31 (1.2.31+ binds the class automatically) * compat: Lower the minimum Cacti compatibility floor from 1.2.32 to 1.2.29 * issue: Re-introduce the `plugin_webseer_contacts` `user_id_type` UNIQUE key (on `user_id`, `type`) that Cacti 1.2.29-1.2.31's `db_update_table()` silently dropped - those releases rebuild a table's indexes only from its `keys` definition and ignore `unique_keys`. The repair runs automatically on upgrade when running one of those releases and is a no-op on 1.2.32+. WARNING: if duplicate `(user_id, type)` contact rows were inserted while the constraint was absent, re-adding the key fails; the upgrade logs the affected table and leaves the recorded version unchanged so it retries on the next request. To recover, de-duplicate `plugin_webseer_contacts` (keep a single row per `user_id`/`type`) and re-run the upgrade. Sites already upgraded to 1.2.32+ that lost the key on an earlier 1.2.29-1.2.31 run are not auto-repaired * feature: Add a root `manifest.json` and an upgrade-time file prune that removes dev-only bundled files (e.g. `tests/`) on a version change, refuses any path resolving outside the plugin directory, and logs anything it cannot remove diff --git a/README.md b/README.md index e0b90c3..ae34f90 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,20 @@ plugin so you can setup maintenance schedules so that known times when a service is going to be down can be configured so that escallation does not needlessly take place during maintenance periods. +## Cacti compatibility + +If you are running a version of Cacti below 1.2.31, please add the function +below to the `applySkin()` function in `include/layout.js` to enable the Cancel +buttons on forms to work: + +```js +$(document).off('click.cactiReturnTo', '.cactiReturnTo') + .on('click.cactiReturnTo', '.cactiReturnTo', function(event) { + event.preventDefault(); + cactiReturnTo($(this).attr('data-url')); + }); +``` + ## Installation To install the webseer plugin, simply copy the plugin_webseer directory to diff --git a/webseer.php b/webseer.php index 525beee..d580c95 100644 --- a/webseer.php +++ b/webseer.php @@ -214,7 +214,7 @@ function form_actions() { \n"; - $save_html = " "; + $save_html = " "; } elseif ($action == WEBSEER_ACTION_URL_DISABLE) { print " @@ -223,7 +223,7 @@ function form_actions() { \n"; - $save_html = " "; + $save_html = " "; } elseif ($action == WEBSEER_ACTION_URL_ENABLE) { print " @@ -232,7 +232,7 @@ function form_actions() { \n"; - $save_html = " "; + $save_html = " "; } elseif ($action == WEBSEER_ACTION_URL_DUPLICATE) { print " @@ -241,7 +241,7 @@ function form_actions() { \n"; - $save_html = " "; + $save_html = " "; } } else { raise_message(40); diff --git a/webseer_proxies.php b/webseer_proxies.php index 903bee3..0872501 100644 --- a/webseer_proxies.php +++ b/webseer_proxies.php @@ -129,7 +129,7 @@ function proxy_form_actions() { \n"; - $save_html = " "; + $save_html = " "; } } else { raise_message(40); diff --git a/webseer_servers.php b/webseer_servers.php index 311588c..ce7688b 100644 --- a/webseer_servers.php +++ b/webseer_servers.php @@ -171,7 +171,7 @@ function form_actions() { "; - $save_html = " "; + $save_html = " "; } elseif ($action == WEBSEER_ACTION_SERVER_DISABLE) { print " @@ -180,7 +180,7 @@ function form_actions() { "; - $save_html = " "; + $save_html = " "; } elseif ($action == WEBSEER_ACTION_SERVER_ENABLE) { print " @@ -189,7 +189,7 @@ function form_actions() { "; - $save_html = " "; + $save_html = " "; } } else { raise_message(40); From 1c1def17059a1be286c9f5ad12b96a5674270b50 Mon Sep 17 00:00:00 2001 From: TheWitness Date: Wed, 7 Oct 2026 18:25:05 -0400 Subject: [PATCH 2/2] test: add CSP cactiReturnTo cancel button regression coverage Assert the three confirmation-page entry points carry no inline cactiReturnTo onClick handler and expose all eight Cancel buttons via the CSP-safe cactiReturnTo class. These files are exempt from the patch-coverage gate, so this source-level test is what catches a reintroduced inline handler. --- tests/Security/CspCancelButtonsTest.php | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/Security/CspCancelButtonsTest.php diff --git a/tests/Security/CspCancelButtonsTest.php b/tests/Security/CspCancelButtonsTest.php new file mode 100644 index 0000000..43cd466 --- /dev/null +++ b/tests/Security/CspCancelButtonsTest.php @@ -0,0 +1,50 @@ + 4, + 'webseer_servers.php' => 3, + 'webseer_proxies.php' => 1, +); + +it('has removed every inline cactiReturnTo onClick handler', function () use ($cancel_button_files) { + foreach (array_keys($cancel_button_files) as $file) { + $source = plugin_test_read_source($file); + + expect($source)->not->toMatch('/onClick\s*=\s*[\'"]cactiReturnTo\(/i', "{$file} still contains an inline cactiReturnTo onClick handler"); + } +}); + +it('binds every Cancel button through the CSP-safe cactiReturnTo class', function () use ($cancel_button_files) { + $total = 0; + + foreach ($cancel_button_files as $file => $expected) { + $source = plugin_test_read_source($file); + + $count = preg_match_all('/class\s*=\s*[\'"]cactiReturnTo[\'"]/i', $source); + + expect($count)->toBe($expected, "{$file} should expose {$expected} cactiReturnTo class button(s), found {$count}"); + + $total += $count; + } + + expect($total)->toBe(8); +});