diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 86237dcf..d22ab2d8 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -40,6 +40,7 @@ jobs: uses: actions/checkout@v4 with: repository: Cacti/cacti + ref: release/1.2.31 path: cacti - name: Checkout Syslog Plugin @@ -57,7 +58,7 @@ jobs: - name: Check PHP Syntax (Lint) run: | cd cacti/plugins/syslog - if find . -name '*.php' -not -path './vendor/*' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then + if find . -name '*.php' -not -path './vendor/*' -exec php -l {} \; 2>&1 | grep -iv 'no syntax errors detected'; then echo "Syntax errors found!" exit 1 fi @@ -122,6 +123,7 @@ jobs: uses: actions/checkout@v4 with: repository: Cacti/cacti + ref: release/1.2.31 path: cacti - name: Checkout Syslog Plugin @@ -143,7 +145,7 @@ jobs: run: sudo apt-get update - name: Install System Dependencies - run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping libapache2-mod-php${{ matrix.php }} + run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping - name: Start SNMPD Agent and Test run: | @@ -163,16 +165,15 @@ jobs: echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf - name: Initialize Cacti Database - env: - MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf' run: | - mysql $MYSQL_AUTH_USR -e 'CREATE DATABASE IF NOT EXISTS cacti;' - mysql $MYSQL_AUTH_USR -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';" - mysql $MYSQL_AUTH_USR -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';" - mysql $MYSQL_AUTH_USR -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';" - mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;" - mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql - mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti + MYSQL_AUTH_USR="--defaults-file=$HOME/.my.cnf" + mysql "$MYSQL_AUTH_USR" -e 'CREATE DATABASE IF NOT EXISTS cacti;' + mysql "$MYSQL_AUTH_USR" -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';" + mysql "$MYSQL_AUTH_USR" -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';" + mysql "$MYSQL_AUTH_USR" -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';" + mysql "$MYSQL_AUTH_USR" -e "FLUSH PRIVILEGES;" + mysql "$MYSQL_AUTH_USR" cacti < ${{ github.workspace }}/cacti/cacti.sql + mysql "$MYSQL_AUTH_USR" -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti - name: Validate composer files run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a22b427..6be2679b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * issue#252: hardening: escape device hostname output in syslog view; parameterize alert API functions * issue#256: hardening: prevent CSV formula injection and malformed CSV output in exports * issue#258: Execute CREATE TABLE SQL correctly during replication sync +* issue#259: hardening: require POST and a CSRF token for the purge syslog devices utility * issue#260: hardening: replace eval-based callback execution in syslog autocomplete JS * issue#278: Extract duplicated alert command execution paths in syslog_process_alerts * issue#278: Extract alert command execution into shared helper in functions.php; command tokenization now uses preg_split (handles tabs and consecutive spaces); /bin/sh fallback for non-executable command templates removed (use absolute paths with execute bit set) diff --git a/functions.php b/functions.php index 1e54c132..032ae97c 100644 --- a/functions.php +++ b/functions.php @@ -47,6 +47,18 @@ function syslog_include_js() { block, because the browser does + * not HTML-decode there. The value has to arrive as a JSON literal. + * + * @param mixed $value + * + * @return string + */ +function syslog_json_safe($value) { + return json_encode($value, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR); +} + function syslog_allow_edits() { global $config; diff --git a/setup.php b/setup.php index 46d0333d..275eb087 100644 --- a/setup.php +++ b/setup.php @@ -1607,7 +1607,31 @@ function syslog_utilities_action($action) { return; } - if ($action == 'purge_syslog_hosts') { + if ($action === 'purge_syslog_hosts') { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + cacti_log('WARNING: syslog purge blocked -- non-POST request', false, 'SYSLOG'); + raise_message('syslog_method_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR); + header('Location: utilities.php?header=false'); + exit; + } + + // csrf_check($fatal) returns bool; $fatal=false tells the helper not to + // die/exit on failure so we can log and redirect with a user-visible + // message ourselves. + if (!function_exists('csrf_check')) { + cacti_log('WARNING: syslog purge blocked -- CSRF validation unavailable', false, 'SYSLOG'); + raise_message('syslog_csrf_unavailable', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR); + header('Location: utilities.php?header=false'); + exit; + } + + if (!csrf_check(false)) { + cacti_log('WARNING: syslog purge blocked -- CSRF token validation failed', false, 'SYSLOG'); + raise_message('syslog_csrf_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR); + header('Location: utilities.php?header=false'); + exit; + } + $records = 0; syslog_db_execute('DELETE FROM syslog_hosts @@ -1642,7 +1666,7 @@ function syslog_utilities_action($action) { raise_message('syslog_info', __('There were %s Device records removed from the Syslog database', $records, 'syslog'), MESSAGE_LEVEL_INFO); - header('Location: utilities.php'); + header('Location: utilities.php?header=false'); exit; } @@ -1660,7 +1684,52 @@ function syslog_utilities_list() {