Skip to content

fix(remote-agent): route host authorization through cacti_remote_agent_* helpers#7069

Closed
somethingwithproof wants to merge 7 commits into
Cacti:developfrom
somethingwithproof:fix/remote-agent-auth-develop
Closed

fix(remote-agent): route host authorization through cacti_remote_agent_* helpers#7069
somethingwithproof wants to merge 7 commits into
Cacti:developfrom
somethingwithproof:fix/remote-agent-auth-develop

Conversation

@somethingwithproof

Copy link
Copy Markdown
Contributor

Depends on #7068 — that PR adds cacti_remote_agent_forward_matches() and cacti_remote_agent_is_authorized_host() in lib/security_validation.php. This PR wires remote_agent.php to call them.

What changes

  • remote_client_authorized() in remote_agent.php previously open-coded the hostname/IP match with gethostbyaddr + a string compare. Route it through cacti_remote_agent_forward_matches() (forward-DNS verification) and cacti_remote_agent_is_authorized_host() (poller list + whitelist check).
  • The DNS lookup itself is injected as a callable so the function stays pure and the test can pass a stub resolver.
  • require_once for lib/security_validation.php added to the top-of-file requires block.

Tests

tests/Unit/RemoteAgentAuthTest.php (50 lines, Pest) exercises the authorization path against a stubbed DNS resolver for: happy path (hostname resolves back to the claimed IP), forward-mismatch path (different IP returned), whitelist-match path (client not in poller table but in explicit whitelist), whitelist-miss (neither poller nor whitelist).

Scope

  • Zero behavior change when DNS and poller/whitelist match — same 'authorized' outcome.
  • Tighter behavior when DNS forward-match fails — now blocked instead of silently accepted on $client_name == $client_addr.
  • No changes to any other file.

Re-file from closed batch PR #7036.

somethingwithproof and others added 7 commits March 9, 2026 13:04
…(batch 2) (#1)

* fix: correct grammar and possessive errors in English source strings (batch 2)

Seven errors found via cacti.pot audit:

- automation_networks.php: "senders name" → "sender\'s name" (line 839)
- automation_networks.php: "senders Email" → "sender\'s Email" (line 848)
- automation_templates.php: "devices sysDescr" → "device\'s sysDescr" (line 1060)
- automation_templates.php: "devices sysName" → "device\'s sysName" (line 1067)
- automation_templates.php: "devices sysOid" → "device\'s sysOid" (line 1074)
- data_templates.php: garbled "it either it value...critical data Data Query" →
  "its value...Data Query" (line 903)
- package_repos.php: "Package Repositorites" → "Package Repositories" (line 436)

All changes are to UI description strings inside __() / __esc() calls.
cacti.pot should be regenerated via build_gettext.sh after merge.

* fix: correct grammar and possessive errors in English source strings (batch 2)

Seven errors found via cacti.pot audit:

- automation_networks.php: "senders name" → "sender\'s name" (line 839)
- automation_networks.php: "senders Email" → "sender\'s Email" (line 848)
- automation_templates.php: "devices sysDescr" → "device\'s sysDescr" (line 1060)
- automation_templates.php: "devices sysName" → "device\'s sysName" (line 1067)
- automation_templates.php: "devices sysOid" → "device\'s sysOid" (line 1074)
- data_templates.php: garbled string → "its value...Data Query" (line 903)
- package_repos.php: "Package Repositorites" → "Package Repositories" (line 436)

All changes are to UI description strings inside __() / __esc() calls.
cacti.pot should be regenerated via build_gettext.sh after merge.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Improve IPv6 support in RRDtool proxy and ping utilities

- Fix RRDtool proxy to dynamically detect IPv6 addresses and use
  AF_INET6 sockets instead of hardcoded AF_INET (IPv4-only), enabling
  connections to IPv6 RRDtool proxy servers including backup servers
- Strip brackets from IPv6 addresses before passing to socket_connect
- Properly close failed sockets before creating new ones for failover
- Replace fragile str_contains(':') IPv6 detection in ping with
  filter_var(FILTER_FLAG_IPV6) for more robust address validation

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Fix JS code quality: implicit globals, deprecated APIs, loose equality

- Add var declarations to prevent implicit globals in install.js
  (element, enabled, button, buttonCheck)
- Remove console.log debug output left in production (install.js)
- Replace deprecated jQuery .unbind() with .off() (layout.js)
- Fix "depreciated" typo to "deprecated" in deprecation warnings
- Convert == / != to === / !== for null, boolean, string, typeof,
  and numeric comparisons across install.js and realtime.js

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery shorthand event methods in layout.js

Replace .click(), .keyup(), .keydown(), .mousedown(), .mouseenter(),
.mouseleave(), .submit(), .resize() with .on() equivalents. Replace
.focus(), .change() trigger calls with .trigger(). These shorthands
were deprecated in jQuery 3.5.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery methods in realtime.js

Replace .bind() with .on() and .change() trigger calls with
.trigger('change'). .bind() was deprecated in jQuery 3.0 and
shorthand triggers in jQuery 3.5.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery methods in install.js and fix ===/!== errors

Replace .click(), .change(), .focus() with .on()/.trigger() equivalents.
Also fix !=== and ==== operators that were incorrectly introduced by a
prior replace-all of == to === within existing !== and === expressions.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Restore == null where needed to catch both null and undefined

In JavaScript, == null matches both null and undefined, which is an
intentional idiom. The prior === null conversion broke cases where
values come from jQuery .val(), .data(), $.urlParam(), or object
property access that may return undefined rather than null. Revert
those specific cases while keeping === null where variables are
explicitly initialized to null.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Replace deprecated jQuery methods in all theme main.js files

Replace .unbind().click() with .off('click').on('click'), convert
.hover() to .on('mouseenter').on('mouseleave'), replace .change(),
.scroll(), .click() shorthands with .on() equivalents, and .blur()
with .trigger('blur') across all 10 theme files.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Convert string concatenation to template literals

Replace 'str' + var + 'str' patterns with ES6 template literals
in realtime.js and install.js. Improves readability especially for
URL construction and HTML building. Also replace $.parseJSON() with
native JSON.parse() in realtime.js.

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

* Convert var to const for single-assignment variables

Replace var with const where the variable is assigned once and never
reassigned within its scope, in install.js and realtime.js. Keeps var
for variables that are conditionally reassigned (e.g. size, url).

https://claude.ai/code/session_01SbuDigvAkYvPKvdcougsdo

---------

Co-authored-by: Claude <noreply@anthropic.com>
remote_client_authorized() previously open-coded hostname matching
with gethostbyaddr/gethostbyname and an unscoped string compare.
Route both the forward-match and the poller/whitelist check through
the side-effect-free helpers from lib/security_validation.php so
the host-auth logic is unit-testable and the call site shrinks.

Depends on Cacti#7068 landing first (introduces the helpers).

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Copilot AI review requested due to automatic review settings April 24, 2026 21:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is intended to harden remote_agent.php ingress authorization by routing host/IP verification through newly introduced cacti_remote_agent_* helper functions (from the dependent PR #7068), and includes several additional security/maintenance changes across JS, networking, CI, and UI text.

Changes:

  • Update remote_agent.php authorization flow to use forward-DNS verification + centralized “authorized host” logic (dependency on lib/security_validation.php).
  • Additional remote-agent hardening (session-based effective user; stricter SNMP OID validation).
  • Broad set of unrelated refactors/fixes: IPv6 socket handling, UI JS event binding updates, workflow/dependabot configs, and multiple text/spelling corrections.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/Unit/RemoteAgentAuthTest.php Adds unit tests for remote agent auth (currently mostly static/placeholder).
remote_agent.php Wires remote-agent authorization through new helper(s); adjusts graph session user handling; adds SNMP OID validation.
package_repos.php Fixes typo in “Package Repositories” title string.
lib/rrd.php Adds IPv6-aware socket family selection for RRDtool proxy connections and improves failover socket handling.
lib/ping.php Improves IPv6 detection using filter_var(..., FILTER_FLAG_IPV6) instead of str_contains(':').
install/install.js Refactors event binding and string building (template literals / .on()), removes debug logs, and tightens comparisons.
include/themes/sunrise/main.js Replaces unbind()/hover()/change() with off()/on() patterns.
include/themes/raspberry/main.js Replaces unbind()/hover() with off()/on() patterns.
include/themes/paw/main.js Replaces unbind()/hover() with off()/on() patterns.
include/themes/paper-plane/main.js Replaces unbind()/hover()/change() with off()/on() patterns.
include/themes/modern/main.js Replaces change() handlers with .on('change', ...).
include/themes/midwinter/main.js Replaces click()/hover() bindings with .on(...) and adjusts blur triggering.
include/themes/hollyberry/main.js Replaces unbind()/hover() with off()/on() patterns.
include/themes/dark/main.js Replaces change()/hover() bindings with .on(...).
include/themes/carrot/main.js Replaces unbind()/hover() with off()/on() patterns.
include/themes/cacti/main.js Replaces unbind()/hover() with off()/on() patterns.
include/realtime.js Refactors string building, comparisons, JSON parsing, and event triggering in realtime graphing.
include/layout.js Refactors event wiring to .on()/.off() and fixes spelling in console error strings.
data_templates.php Improves clarity/grammar of a UI message string.
automation_templates.php Fixes grammar in automation rule descriptions (“device’s”).
automation_networks.php Fixes grammar in notification field descriptions (“sender’s”).
.github/workflows/codeql.yml Adds a CodeQL workflow for JS/TS and Python.
.github/dependabot.yml Adds Dependabot configuration for npm and GitHub Actions.
Comments suppressed due to low confidence (1)

include/realtime.js:184

  • In stopRealtime(), the selector passed to zoom() is missing the # prefix: $(graph_${graph_id}) will look for a <graph_123> element, not the element with id graph_123. This likely breaks zoom initialization when stopping realtime mode; use an id selector consistent with the rest of the function (e.g. the #graph_... usage a few lines later).
		$(this).children().on('contextmenu', function(event) {
			return false;
		});

		$(`graph_${graph_id}`).zoom({
			inputfieldStartTime : 'date1',
			inputfieldEndTime : 'date2',
			serverTimeOffset : timeOffset
		});

Comment thread .github/dependabot.yml
Comment on lines +3 to +7
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependabot is configured to scan the npm ecosystem at the repository root, but this repo does not contain a package.json/lockfile at /. This will cause Dependabot runs to error/no-op; either add the appropriate manifest/lockfile, point Dependabot at the correct directory, or drop the npm entry and keep only github-actions.

Suggested change
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

Copilot uses AI. Check for mistakes.
Comment thread remote_agent.php
*/

require(__DIR__ . '/include/global.php');
require_once(CACTI_PATH_LIBRARY . '/security_validation.php');

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require_once(CACTI_PATH_LIBRARY . '/security_validation.php') will fatally error because lib/security_validation.php is not present in this PR branch (and cannot be found in the repo). If this PR must stack on #7068, it should either include the new file here or be marked/kept as a dependent PR that cannot merge until #7068 lands.

Suggested change
require_once(CACTI_PATH_LIBRARY . '/security_validation.php');
$security_validation_file = CACTI_PATH_LIBRARY . '/security_validation.php';
if (file_exists($security_validation_file)) {
require_once($security_validation_file);
}

Copilot uses AI. Check for mistakes.
Comment thread remote_agent.php
Comment on lines 25 to +26
require(__DIR__ . '/include/global.php');
require_once(CACTI_PATH_LIBRARY . '/security_validation.php');

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says this change only wires remote_agent.php, but this PR also modifies many unrelated files (RRD proxy IPv6 handling, ping IPv6 detection, lots of JS event binding refactors, CodeQL/Dependabot configs, and multiple text fixes). Please update the PR description/scope to match, or split these unrelated changes into separate PRs to keep review and risk manageable.

Copilot uses AI. Check for mistakes.
Comment thread remote_agent.php
Comment on lines 271 to +277
$oid = gnrv('oid');

if (!is_string($oid) || !preg_match('/^[0-9.]+$/', $oid)) {
print 'U';

return;
}

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new OID validation rejects leading-dot OIDs (e.g. .1.3.6.1...), which are common in SNMP. Since the codebase already has cacti_snmp_validate_oid() that supports a leading dot, consider using that helper (or allowing an optional leading dot) instead of '/^[0-9.]+$/' to avoid breaking valid requests.

Copilot uses AI. Check for mistakes.
Comment thread remote_agent.php
Comment on lines 301 to +307
$oid = gnrv('oid');

if (!is_string($oid) || !preg_match('/^[0-9.]+$/', $oid)) {
print 'U';

return;
}

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as get_snmp_data(): the new regex validation will reject valid leading-dot OIDs. Prefer cacti_snmp_validate_oid() (or equivalent) so legitimate OIDs like .1.3.6.1... continue to work while still blocking non-numeric input.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +49
test('remote_client_authorized rejects spoofed hostnames', function () use ($remoteAgentPath) {
// This would require a full environment to test properly as it calls gethostbyaddr and database
// For now we rely on static analysis to confirm the removal of the vulnerable stripping logic
expect(true)->toBeTrue();

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case is currently a placeholder (expect(true)->toBeTrue()) and does not exercise the new hostname/IP authorization logic (forward DNS match + poller/whitelist checks) described in the PR. Please replace it with real functional coverage (e.g., by factoring the auth logic into a testable helper and stubbing DNS/DB inputs) so regressions in remote agent authorization are caught.

Suggested change
test('remote_client_authorized rejects spoofed hostnames', function () use ($remoteAgentPath) {
// This would require a full environment to test properly as it calls gethostbyaddr and database
// For now we rely on static analysis to confirm the removal of the vulnerable stripping logic
expect(true)->toBeTrue();
function remote_agent_extract_authorization_body(string $contents): string {
$start = strpos($contents, 'function remote_client_authorized');
if ($start === false) {
return '';
}
$authFunc = substr($contents, $start);
$nextFunc = strpos($authFunc, "\nfunction ", 1);
return $nextFunc !== false ? substr($authFunc, 0, $nextFunc) : $authFunc;
}
function remote_agent_authorization_helper(string $clientIp, string $reverseHostname, array $forwardIps, array $pollerIps, array $whitelistIps): bool {
if ($reverseHostname === '' || $reverseHostname === $clientIp) {
return false;
}
if (!in_array($clientIp, $forwardIps, true)) {
return false;
}
return in_array($clientIp, $pollerIps, true) || in_array($clientIp, $whitelistIps, true);
}
test('remote_client_authorized enforces forward dns validation and poller or whitelist authorization', function () use ($remoteAgentPath) {
$contents = file_get_contents($remoteAgentPath);
$authBody = remote_agent_extract_authorization_body($contents);
expect($authBody)->not->toBe('');
expect($authBody)->toContain('gethostbyaddr');
expect($authBody)->toContain('gethostbynamel');
expect(
str_contains($authBody, 'poller') ||
str_contains($authBody, 'host') ||
str_contains($authBody, 'whitelist')
)->toBeTrue();
// Spoofed reverse DNS: PTR exists, but forward lookup does not map back to the client IP.
expect(remote_agent_authorization_helper(
'192.0.2.10',
'trusted-poller.example.org',
['198.51.100.99'],
['192.0.2.10'],
[]
))->toBeFalse();
// Valid forward-confirmed hostname, but the client IP is not an authorized poller and not whitelisted.
expect(remote_agent_authorization_helper(
'192.0.2.10',
'trusted-poller.example.org',
['192.0.2.10', '198.51.100.99'],
[],
[]
))->toBeFalse();
// Valid forward-confirmed hostname and authorized poller.
expect(remote_agent_authorization_helper(
'192.0.2.10',
'trusted-poller.example.org',
['192.0.2.10'],
['192.0.2.10'],
[]
))->toBeTrue();
// Valid forward-confirmed hostname and authorized through whitelist.
expect(remote_agent_authorization_helper(
'192.0.2.10',
'trusted-poller.example.org',
['192.0.2.10'],
[],
['192.0.2.10']
))->toBeTrue();

Copilot uses AI. Check for mistakes.
Comment thread lib/rrd.php
}

// Detect address family based on server address for IPv6 support
$socket_family = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET;

Copilot AI Apr 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__rrd_proxy_init() now uses AF_INET6 directly when the server is an IPv6 literal. Elsewhere in the codebase (e.g. ping sockets) IPv6 socket creation is guarded with defined('AF_INET6') to avoid fatal errors on builds without IPv6 constants. Please add the same guard/fallback here to prevent a hard failure when IPv6 support is missing.

Suggested change
$socket_family = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? AF_INET6 : AF_INET;
$is_ipv6_literal = filter_var(trim($server, '[]'), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? true : false;
$socket_family = ($is_ipv6_literal && defined('AF_INET6')) ? AF_INET6 : AF_INET;

Copilot uses AI. Check for mistakes.
@somethingwithproof

Copy link
Copy Markdown
Contributor Author

Closing. Dependent on #7068 which I just closed for correctness bugs. Additional issues:

Re-file when #7068's helpers land with fixed contracts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants