mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
ci(security): consolidated Semgrep + Psalm taint gate (#7306)
* ci(security): add consolidated Semgrep and Psalm taint gate One workflow runs both taint scanners as separate jobs. Semgrep and Psalm treat htmle()/html_escape()/sanitize_uri() as sanitizers, so findings mean request input reaches a browser sink unescaped. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * stabilize psalm taint analysis * Install dependencies before Psalm analysis * Remove obsolete Psalm taint baseline --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
1 parent
a9748b92b8
commit
9476a306de
6 files changed
+272
No files matched your search
@@ -0,0 +1,86 @@
|
||||
# +-------------------------------------------------------------------------+
|
||||
# | Copyright (C) 2004-2026 The Cacti Group |
|
||||
# | |
|
||||
# | This program is free software; you can redistribute it and/or |
|
||||
# | modify it under the terms of the GNU General Public License |
|
||||
# | as published by the Free Software Foundation; either version 2 |
|
||||
# | of the License, or (at your option) any later version. |
|
||||
# +-------------------------------------------------------------------------+
|
||||
# | Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
# +-------------------------------------------------------------------------+
|
||||
|
||||
name: Security SAST
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop, 1.2.x]
|
||||
pull_request:
|
||||
branches: [develop, 1.2.x]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: security-sast-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Semgrep taint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Semgrep
|
||||
run: python3 -m pip install semgrep==1.152.0
|
||||
|
||||
- name: Run Semgrep
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${BASE_SHA:-}" ]; then
|
||||
semgrep --config .semgrep.yml --error --baseline-commit "$BASE_SHA" .
|
||||
else
|
||||
semgrep --config .semgrep.yml --error .
|
||||
fi
|
||||
|
||||
psalm:
|
||||
name: Psalm taint
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
COMPOSER_ALLOW_SUPERUSER: 1
|
||||
PSALM_VERSION: '6.16.1'
|
||||
PSALM_PHAR_SHA256: '7ace71e4f698466e7e16810679a1895ea623060579aff46fcf9fd0f1c09abe00'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
|
||||
|
||||
- name: Setup PHP 8.3
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.3'
|
||||
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring
|
||||
coverage: none
|
||||
|
||||
- name: Install Composer dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Download and verify pinned Psalm phar
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o psalm.phar \
|
||||
"https://github.com/vimeo/psalm/releases/download/${PSALM_VERSION}/psalm.phar"
|
||||
echo "${PSALM_PHAR_SHA256} psalm.phar" | sha256sum -c -
|
||||
|
||||
- name: Run Psalm taint analysis
|
||||
run: php psalm.phar --taint-analysis --no-progress
|
||||
@@ -0,0 +1,69 @@
|
||||
# Cacti-aware Semgrep rules.
|
||||
#
|
||||
# Taint-mode rules that understand Cacti's own escapers, so findings are
|
||||
# limited to request input that reaches a browser sink without passing through
|
||||
# one of them. htmle()/html_escape() run htmlspecialchars(); htmlerv() and
|
||||
# html_escape_request_var() are their request-var wrappers; sanitize_uri()
|
||||
# cleans redirect targets. get_request_var() and get_nfilter_request_var()
|
||||
# both return $_REQUEST values untouched (the filtering variant is
|
||||
# get_filter_request_var()), so they are unsanitized sources.
|
||||
rules:
|
||||
- id: cacti-reflected-xss
|
||||
mode: taint
|
||||
languages: [php]
|
||||
severity: ERROR
|
||||
message: >-
|
||||
Request input reaches echo/print without an HTML escaper. Wrap the value
|
||||
in htmle()/html_escape() (or htmlerv()/html_escape_request_var() for
|
||||
request vars) before output to prevent reflected XSS.
|
||||
metadata:
|
||||
cwe: "CWE-79: Improper Neutralization of Input During Web Page Generation (Cross-site Scripting)"
|
||||
owasp: "A03:2021 - Injection"
|
||||
category: security
|
||||
confidence: MEDIUM
|
||||
pattern-sources:
|
||||
- pattern: $_GET[...]
|
||||
- pattern: $_POST[...]
|
||||
- pattern: $_REQUEST[...]
|
||||
- pattern: get_request_var(...)
|
||||
- pattern: get_nfilter_request_var(...)
|
||||
pattern-sanitizers:
|
||||
- pattern: htmle(...)
|
||||
- pattern: html_escape(...)
|
||||
- pattern: html_escape_request_var(...)
|
||||
- pattern: htmlerv(...)
|
||||
- pattern: sanitize_uri(...)
|
||||
pattern-sinks:
|
||||
- pattern: echo $SINK;
|
||||
- pattern: print $SINK;
|
||||
|
||||
- id: cacti-header-injection
|
||||
mode: taint
|
||||
languages: [php]
|
||||
severity: ERROR
|
||||
message: >-
|
||||
Request input reaches header() without sanitization. Validate or escape
|
||||
the value (e.g. sanitize_uri() for redirect targets) before setting a
|
||||
response header to prevent header injection and open redirects.
|
||||
metadata:
|
||||
cwe: "CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers (HTTP Response Splitting)"
|
||||
owasp: "A03:2021 - Injection"
|
||||
category: security
|
||||
confidence: MEDIUM
|
||||
# Only raw superglobals are sources here. The get_*_request_var() helpers
|
||||
# feed the standard redirect idiom (header('Location: page.php?id=' .
|
||||
# get_request_var('id'))) where 'id' is separately int-validated via
|
||||
# get_filter_request_var(), so treating them as header sinks is low signal.
|
||||
# A raw superglobal concatenated into header() is the genuine injection.
|
||||
pattern-sources:
|
||||
- pattern: $_GET[...]
|
||||
- pattern: $_POST[...]
|
||||
- pattern: $_REQUEST[...]
|
||||
pattern-sanitizers:
|
||||
- pattern: htmle(...)
|
||||
- pattern: html_escape(...)
|
||||
- pattern: html_escape_request_var(...)
|
||||
- pattern: htmlerv(...)
|
||||
- pattern: sanitize_uri(...)
|
||||
pattern-sinks:
|
||||
- pattern: header(...)
|
||||
@@ -0,0 +1,7 @@
|
||||
# Third-party and generated assets. CLI scripts are not listed here on
|
||||
# purpose: they take filenames from argv, which the web-sink rules in
|
||||
# .semgrep.yml do not match, so no blanket ignore is needed for them.
|
||||
include/vendor/
|
||||
include/fa/
|
||||
node_modules/
|
||||
tests/e2e/node_modules/
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Psalm taint-analysis stubs for Cacti's output escapers.
|
||||
*
|
||||
* These declarations exist only to teach Psalm where tainted data is made
|
||||
* safe. The real implementations live in lib/html.php and lib/functions.php;
|
||||
* the signatures below must stay in sync with them. Without these escape
|
||||
* markers Psalm reports XSS taint on every value that is correctly passed
|
||||
* through htmlspecialchars() by way of these helpers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* html_escape() runs the value through htmlspecialchars() with ENT_QUOTES.
|
||||
*
|
||||
* @psalm-taint-escape html
|
||||
*/
|
||||
function html_escape(mixed $string = '') : string {}
|
||||
|
||||
/**
|
||||
* htmle() is a thin alias for html_escape().
|
||||
*
|
||||
* @psalm-taint-escape html
|
||||
*/
|
||||
function htmle(mixed $string) : string {}
|
||||
|
||||
/**
|
||||
* sanitize_uri() strips the characters used to break out of HTML and URL
|
||||
* contexts (angle brackets, quotes, backticks, parentheses, etc.), so both
|
||||
* the html sink and the quote-bearing text sink are satisfied.
|
||||
*
|
||||
* @psalm-taint-escape html
|
||||
* @psalm-taint-escape has_quotes
|
||||
*/
|
||||
function sanitize_uri(string $uri) : string {}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="6.16.1@f1f5de594dc76faf8784e02d3dc4716c91c6f6ac">
|
||||
<file src="include/csrf.php">
|
||||
<TaintedHeader>
|
||||
<code><![CDATA['Location: ' . sanitize_uri($_SERVER['REQUEST_URI'])]]></code>
|
||||
</TaintedHeader>
|
||||
</file>
|
||||
<file src="include/global.php">
|
||||
<TaintedHeader>
|
||||
<code><![CDATA["Content-Security-Policy: default-src *; img-src 'self' https://api.qrserver.com $alternates data: blob:; style-src 'self' 'unsafe-inline' $alternates; script-src 'self' $script_policy 'unsafe-inline' $alternates; frame-ancestors 'self'; worker-src 'self' $alternates;"]]></code>
|
||||
</TaintedHeader>
|
||||
</file>
|
||||
<file src="include/global_session.php">
|
||||
<TaintedHtml>
|
||||
<code><![CDATA[gfrv('action', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^([-a-zA-Z0-9_\s]+)$/']])]]></code>
|
||||
</TaintedHtml>
|
||||
<TaintedTextWithQuotes>
|
||||
<code><![CDATA[gfrv('action', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^([-a-zA-Z0-9_\s]+)$/']])]]></code>
|
||||
</TaintedTextWithQuotes>
|
||||
</file>
|
||||
<file src="install/functions.php">
|
||||
<TaintedFile>
|
||||
<code><![CDATA[$cacheFile]]></code>
|
||||
</TaintedFile>
|
||||
<TaintedHtml>
|
||||
<code><![CDATA[$data]]></code>
|
||||
</TaintedHtml>
|
||||
</file>
|
||||
<file src="lib/auth.php">
|
||||
<TaintedHeader>
|
||||
<code><![CDATA['Location: ' . $referer]]></code>
|
||||
</TaintedHeader>
|
||||
</file>
|
||||
<file src="lib/functions.php">
|
||||
<TaintedHtml>
|
||||
<code><![CDATA[$omessage]]></code>
|
||||
<code><![CDATA['Message: ' . trim($string) . PHP_EOL]]></code>
|
||||
</TaintedHtml>
|
||||
<TaintedTextWithQuotes>
|
||||
<code><![CDATA[$omessage]]></code>
|
||||
<code><![CDATA['Message: ' . trim($string) . PHP_EOL]]></code>
|
||||
</TaintedTextWithQuotes>
|
||||
</file>
|
||||
</files>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="8"
|
||||
resolveFromConfigFile="true"
|
||||
autoloader="include/vendor/autoload.php"
|
||||
errorBaseline="psalm-baseline.xml"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="lib" />
|
||||
<directory name="install" />
|
||||
<directory name="cli" />
|
||||
<directory name="include" />
|
||||
<file name="managers.php" />
|
||||
<file name="package_import.php" />
|
||||
<file name="graphs.php" />
|
||||
<file name="host.php" />
|
||||
<file name="reports.php" />
|
||||
<file name="graph_view.php" />
|
||||
<file name="data_sources.php" />
|
||||
<file name="aggregate_graphs.php" />
|
||||
<ignoreFiles>
|
||||
<directory name="include/vendor" />
|
||||
<directory name="include/fa" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
|
||||
<stubs>
|
||||
<file name="build/psalm/cacti-sanitizers.phpstub" />
|
||||
</stubs>
|
||||
</psalm>
|
||||
Reference in new issue
Block a user