Skip to content

hardening: enforce POST+CSRF for purge syslog devices utility#318

Open
somethingwithproof wants to merge 3 commits intoCacti:developfrom
somethingwithproof:fix/259-csrf-purge-utility
Open

hardening: enforce POST+CSRF for purge syslog devices utility#318
somethingwithproof wants to merge 3 commits intoCacti:developfrom
somethingwithproof:fix/259-csrf-purge-utility

Conversation

@somethingwithproof
Copy link
Copy Markdown
Contributor

@somethingwithproof somethingwithproof commented Apr 11, 2026

Summary

Re-opens work from #265 (self-closed, never merged into main). Enforces POST + CSRF token validation before the purge_syslog_hosts utility action executes the DELETE FROM syslog_hosts query, and replaces the GET anchor link with a jQuery UI dialog + POST flow.

Changes

setup.php — action handler and UI:

  • Require REQUEST_METHOD === 'POST' before executing the purge; GET/HEAD requests log a warning and redirect back to utilities.php.
  • When csrf_check() is available, call csrf_check(false) (the $fatal=false arg tells the helper to return false on failure rather than die/exit, so we can log and redirect with a user-visible message).
  • Fail-closed when the csrf helper is missing: log + redirect, no attempt at manual token comparison.
  • Distinct internal message IDs per failure mode (syslog_method_error, syslog_csrf_error, syslog_csrf_unavailable) so log triage can differentiate non-POST, invalid token, and missing-helper paths.
  • Replace the <a class='hyperLink' href='utilities.php?action=purge_syslog_hosts'> GET link with a jQuery UI dialog matching the displayDialog() pattern from Cacti core. The dialog posts via postUrl() / loadPageUsingPost() with __csrf_magic: csrfMagicToken.
  • All translated strings in script context use json_encode(..., JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) to prevent </script> breakout.

tests/regression/issue259_csrf_purge_test.php — source-scan lint:

  • Asserts the expected POST + csrf_check + JSON hex flag strings exist in setup.php.
  • Explicitly documented as a lint, NOT a behavioral test. A full behavioral test would require bootstrapping Cacti's session, CSRF token, and DB which this plugin's regression harness does not provide. Follow-up issue for real behavioral coverage once a DB-backed test harness exists.

Validation

  • php -l setup.php
  • php tests/regression/issue259_csrf_purge_test.php
  • CI: PHP Quality (Lint + PHPStan + CS-Fixer) ✓, PHP 8.1/8.2/8.3 Integration Tests ✓

Fixes #259

Copilot AI review requested due to automatic review settings April 11, 2026 08:25
Refs Cacti#259

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 hardens the Syslog plugin’s destructive purge_syslog_hosts utilities action to mitigate CSRF risk by requiring a CSRF-protected POST, and updates the UI flow to submit a POST (instead of a GET link). It also adds a regression script to guard against reintroducing the insecure behavior.

Changes:

  • Enforce POST and validate CSRF via csrf_check(false) before executing the purge deletes.
  • Replace the utilities GET link with a jQuery UI confirmation dialog that submits a POST including __csrf_magic.
  • Add a regression script asserting the hardening behavior and related UI/encoding expectations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
setup.php Adds POST+CSRF enforcement for purge action; replaces purge link with a dialog-driven POST flow including CSRF token.
tests/regression/issue259_csrf_purge_test.php Adds a regression script that scans setup.php for required hardening snippets and safe UI/JS encoding patterns.

setup.php Outdated
Comment on lines +1570 to +1577
raise_message('syslog_error', __('Invalid request. This action requires a CSRF protected POST.', 'syslog'), MESSAGE_LEVEL_ERROR);
header('Location: utilities.php');
exit;
}

if (function_exists('csrf_check')) {
if (!csrf_check(false)) {
raise_message('syslog_error', __('Invalid request. This action requires a CSRF protected POST.', 'syslog'), MESSAGE_LEVEL_ERROR);
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The user-facing error message for invalid purge requests includes the phrase "CSRF protected POST". To avoid leaking security mechanism details (and to align with the regression test intent that end-user messages should be generic), consider using a generic message (e.g., "Invalid request. Please try again.") and logging the specific failure reason via cacti_log() instead.

Suggested change
raise_message('syslog_error', __('Invalid request. This action requires a CSRF protected POST.', 'syslog'), MESSAGE_LEVEL_ERROR);
header('Location: utilities.php');
exit;
}
if (function_exists('csrf_check')) {
if (!csrf_check(false)) {
raise_message('syslog_error', __('Invalid request. This action requires a CSRF protected POST.', 'syslog'), MESSAGE_LEVEL_ERROR);
cacti_log('WARNING: syslog purge blocked -- invalid request method', false, 'SYSLOG');
raise_message('syslog_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR);
header('Location: utilities.php');
exit;
}
if (function_exists('csrf_check')) {
if (!csrf_check(false)) {
cacti_log('WARNING: syslog purge blocked -- CSRF validation failed', false, 'SYSLOG');
raise_message('syslog_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR);

Copilot uses AI. Check for mistakes.
Comment on lines +1575 to +1586
if (function_exists('csrf_check')) {
if (!csrf_check(false)) {
raise_message('syslog_error', __('Invalid request. This action requires a CSRF protected POST.', 'syslog'), MESSAGE_LEVEL_ERROR);
header('Location: utilities.php');
exit;
}
} else {
cacti_log('WARNING: syslog purge blocked -- CSRF validation unavailable', false, 'SYSLOG');
raise_message('syslog_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR);
header('Location: utilities.php');
exit;
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

PR description mentions an "__csrf_magic fallback if csrf helper is unavailable", but the current else-branch when csrf_check() is unavailable hard-blocks the purge action. Either implement the documented fallback behavior (so purge can still work on supported environments without csrf_check) or update the PR description/tests to reflect the intentional fail-closed behavior.

Copilot uses AI. Check for mistakes.
}

// Verify user-facing message does not expose CSRF internals (log message may use "CSRF")
if (strpos($setup, "raise_message('syslog_error', __('CSRF") !== false) {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The regression assertion intended to ensure user-facing errors do not expose CSRF internals is too narrow: it only checks for the literal substring "raise_message('syslog_error', __('CSRF", so it will not fail if the message contains "CSRF" later in the string. Consider strengthening this check to detect any raise_message() call whose translated message contains "CSRF" (or similar security-internal terms).

Suggested change
if (strpos($setup, "raise_message('syslog_error', __('CSRF") !== false) {
if (preg_match('/raise_message\s*\(\s*[^,]+,\s*__\(\s*([\'"])[^\'"]*CSRF[^\'"]*\1/si', $setup)) {

Copilot uses AI. Check for mistakes.
@somethingwithproof somethingwithproof force-pushed the fix/259-csrf-purge-utility branch from f595568 to 75e4ad6 Compare April 11, 2026 08:38
@somethingwithproof
Copy link
Copy Markdown
Contributor Author

@TheWitness could you take a look? This is the re-opened CSRF+POST hardening for the purge devices utility (original #265), now rebased onto develop. All merge conflicts resolved.

…eader

- Distinct raise_message IDs per failure mode (syslog_method_error,
  syslog_csrf_error, syslog_csrf_unavailable) so log triage can
  differentiate non-POST, invalid token, and missing-helper paths
- Add cacti_log entry on the non-POST rejection path so the audit
  trail is symmetric with the other two fail-closed branches
- Document csrf_check($fatal=false) arg semantics inline so future
  readers see the helper contract
- Rename the regression test comment block to call out explicitly
  that it is a source-scan lint, not a behavioral test; flag follow-up
  for real behavioral coverage once a DB-backed test harness exists

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
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.

hardening: require POST + CSRF token for purge_syslog_hosts utility action

2 participants