diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 86237dc..265b555 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 libapache2-mod-php - 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/functions.php b/functions.php index 1e54c13..d39c435 100644 --- a/functions.php +++ b/functions.php @@ -47,6 +47,10 @@ function syslog_include_js() { - + '> + + diff --git a/syslog.php b/syslog.php index 0d1c45c..80165d8 100644 --- a/syslog.php +++ b/syslog.php @@ -1184,11 +1184,11 @@ function syslog_filter($sql_where, $tab) { ?> - + $save_html "; @@ -856,7 +856,7 @@ function syslog_alerts() { 'user' => [__('By User', 'syslog'), 'DESC'] ]; - $nav = html_nav_bar('syslog_alerts.php?filter=' . get_request_var('filter'), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, cacti_sizeof($display_text) + 1, __('Alerts', 'syslog'), 'page', 'main'); + $nav = html_nav_bar('syslog_alerts.php?filter=' . rawurlencode(get_request_var('filter')), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, cacti_sizeof($display_text) + 1, __('Alerts', 'syslog'), 'page', 'main'); form_start('syslog_alerts.php', 'chk'); diff --git a/syslog_removal.php b/syslog_removal.php index e047e68..b8768b5 100644 --- a/syslog_removal.php +++ b/syslog_removal.php @@ -234,7 +234,7 @@ function form_actions() { - + $save_html "; @@ -667,7 +667,7 @@ function syslog_removal() { form_start('syslog_removal.php', 'chk'); - $nav = html_nav_bar('syslog_removal.php?filter=' . get_request_var('filter'), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, cacti_sizeof($display_text) + 1, __('Rules', 'syslog'), 'page', 'main'); + $nav = html_nav_bar('syslog_removal.php?filter=' . rawurlencode(get_request_var('filter')), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, cacti_sizeof($display_text) + 1, __('Rules', 'syslog'), 'page', 'main'); print $nav; diff --git a/syslog_reports.php b/syslog_reports.php index fd7b212..ecf2ef1 100644 --- a/syslog_reports.php +++ b/syslog_reports.php @@ -206,7 +206,7 @@ function form_actions() { - + $save_html \n"; @@ -704,7 +704,7 @@ function syslog_report() { 'user' => [__('By User', 'syslog'), 'DESC'] ]; - $nav = html_nav_bar('syslog_reports.php?filter=' . get_request_var('filter'), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, cacti_sizeof($display_text) + 1, __('Reports', 'syslog'), 'page', 'main'); + $nav = html_nav_bar('syslog_reports.php?filter=' . rawurlencode(get_request_var('filter')), MAX_DISPLAY_PAGES, get_request_var('page'), $rows, $total_rows, cacti_sizeof($display_text) + 1, __('Reports', 'syslog'), 'page', 'main'); form_start('syslog_reports.php', 'chk'); diff --git a/tests/regression/issue259_csrf_purge_test.php b/tests/regression/issue259_csrf_purge_test.php new file mode 100644 index 0000000..e183100 --- /dev/null +++ b/tests/regression/issue259_csrf_purge_test.php @@ -0,0 +1,127 @@ + breakout in HTML script context +if (!str_contains($functions, 'JSON_HEX_TAG')) { + fwrite(STDERR, "json_encode() must use JSON_HEX_TAG to prevent script-context breakout.\n"); + exit(1); +} + +if (!str_contains($functions, 'JSON_HEX_AMP')) { + fwrite(STDERR, "json_encode() must use JSON_HEX_AMP to escape ampersands in script context.\n"); + exit(1); +} + +if (!str_contains($functions, 'JSON_HEX_APOS')) { + fwrite(STDERR, "json_encode() must use JSON_HEX_APOS.\n"); + exit(1); +} + +if (!str_contains($functions, 'JSON_HEX_QUOT')) { + fwrite(STDERR, "json_encode() must use JSON_HEX_QUOT.\n"); + exit(1); +} + +// Verify user-facing messages do not expose CSRF internals (log messages may use "CSRF") +if (preg_match('/raise_message\\s*\\(\\s*[^,]+,\\s*__\\(\\s*([\'\"])[^\'\"]*CSRF[^\'\"]*\\1/si', $setup)) { + fwrite(STDERR, "User-facing raise_message must not expose CSRF internals to end users.\n"); + exit(1); +} + +// Verify generic user-facing message is present +if (!str_contains($setup, 'Invalid request. Please try again.')) { + fwrite(STDERR, "Fail-closed branch must use generic 'Invalid request. Please try again.' message.\n"); + exit(1); +} + +// Verify fail-closed raise_message uses MESSAGE_LEVEL_ERROR severity +if (!str_contains($setup, "raise_message('syslog_csrf_unavailable', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR)")) { + fwrite(STDERR, "Fail-closed branch raise_message must use MESSAGE_LEVEL_ERROR severity.\n"); + exit(1); +} + +// Verify log message does not expose internal function name +if (str_contains($setup, 'csrf_check() unavailable')) { + fwrite(STDERR, "Log message must not name internal validation function.\n"); + exit(1); +} + +echo "issue259_csrf_purge_test passed\n"; diff --git a/tests/regression/issue279_bulk_form_and_nav_encoding_test.php b/tests/regression/issue279_bulk_form_and_nav_encoding_test.php new file mode 100644 index 0000000..48f533c --- /dev/null +++ b/tests/regression/issue279_bulk_form_and_nav_encoding_test.php @@ -0,0 +1,58 @@ + file_get_contents(__DIR__ . '/../../syslog_removal.php'), + 'syslog_alerts.php' => file_get_contents(__DIR__ . '/../../syslog_alerts.php'), + 'syslog_reports.php' => file_get_contents(__DIR__ . '/../../syslog_reports.php'), + 'syslog.php' => file_get_contents(__DIR__ . '/../../syslog.php'), +]; + +foreach ($targets as $file => $contents) { + if ($contents === false) { + fwrite(STDERR, "Unable to read $file\n"); + exit(1); + } +} + +foreach (['syslog_removal.php', 'syslog_alerts.php', 'syslog_reports.php'] as $file) { + if (!str_contains($targets[$file], "html_escape(get_request_var('drp_action'))")) { + fwrite(STDERR, "Expected escaped drp_action hidden field in $file\n"); + exit(1); + } + + if (!str_contains($targets[$file], "rawurlencode(get_request_var('filter'))")) { + fwrite(STDERR, "Expected URL-encoded filter nav value in $file\n"); + exit(1); + } + + if (str_contains($targets[$file], "")) { + fwrite(STDERR, "Legacy raw drp_action hidden field remains in $file\n"); + exit(1); + } +} + +$syslog = $targets['syslog.php']; + +if (!str_contains($syslog, "pageTab: ,")) { + fwrite(STDERR, "Expected JSON-encoded syslog pageTab value\n"); + exit(1); +} + +foreach ([ + "syslog_json_encode_for_script(__('Enter a search term', 'syslog'))", + "syslog_json_encode_for_script(__('Select Device(s)', 'syslog'))", + "syslog_json_encode_for_script(__('Devices Selected', 'syslog'))", + "syslog_json_encode_for_script(__('All Devices Selected', 'syslog'))", +] as $needle) { + if (!str_contains($syslog, $needle)) { + fwrite(STDERR, "Expected JS-safe initSyslogMain text encoding\n"); + exit(1); + } +} + +if (str_contains($syslog, "pageTab: ''")) { + fwrite(STDERR, "Legacy raw pageTab JS assignment still present\n"); + exit(1); +} + +echo "OK\n";