From 899cf3125506866fa2facd0e0ef48e63cda03893 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Sat, 4 Jul 2026 11:10:02 +0200 Subject: [PATCH] Harden CI: validate PR artifact inputs, migrate off deprecated Semgrep action (#5232) * Harden CI workflows: validate PR artifact inputs and migrate off deprecated Semgrep action Address two CI/supply-chain hardening items from the 2026-07-03 security audit: - comment_check_amalgamation.yml (todo 117): the privileged `workflow_run` job consumes an untrusted PR artifact. Validate `author` against a strict GitHub-username pattern and `number` as a positive integer before use, and extract the artifact into a dedicated directory (`unzip -o pr.zip -d ./pr_artifact`), reading only the two expected files by fixed path. This prevents Markdown/mention injection via the attacker-controlled `author` text and avoids a malicious archive touching the workspace. - semgrep.yml (todo 118): `returntocorp/semgrep-action` is deprecated (the org was renamed to `semgrep/*`). Replace it with an explicit `semgrep ci` invocation via the maintained CLI; the deployment is inferred from SEMGREP_APP_TOKEN. Todo 116 (CIFuzz `@master` refs) already carries a comment documenting the OSS-Fuzz-recommended exception, so no change is needed there. Signed-off-by: Niels Lohmann Co-Authored-By: Claude Opus 4.8 * Fix Semgrep step: use `semgrep scan` instead of token-gated `semgrep ci` The CI `Scan` job failed with "Path does not exist: semgrep.sarif" because `semgrep ci` requires a login token (SEMGREP_APP_TOKEN), which this repo does not have configured, so it bailed without producing a SARIF file. The former returntocorp/semgrep-action, given no token, fell back to plain `semgrep scan --sarif`; match that with `semgrep scan --config auto`, which needs no token and always produces the SARIF for upload. Signed-off-by: Niels Lohmann Co-Authored-By: Claude Opus 4.8 --------- Signed-off-by: Niels Lohmann Co-authored-by: Claude Opus 4.8 --- .../workflows/comment_check_amalgamation.yml | 20 +++++++++++++--- .github/workflows/semgrep.yml | 23 ++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.github/workflows/comment_check_amalgamation.yml b/.github/workflows/comment_check_amalgamation.yml index 6ff9fb1e8..30ae044e3 100644 --- a/.github/workflows/comment_check_amalgamation.yml +++ b/.github/workflows/comment_check_amalgamation.yml @@ -47,7 +47,10 @@ jobs: var hasPatch = artifacts.data.artifacts.some((artifact) => artifact.name == "amalgamation-patch"); core.setOutput('has_patch', String(hasPatch)); - - run: unzip pr.zip + # Extract the untrusted PR artifact into a dedicated empty directory and + # read only the two expected files by fixed path afterwards. This avoids a + # malicious archive overwriting workspace files or escaping via ../ paths. + - run: unzip -o pr.zip -d ./pr_artifact - name: 'Comment on PR' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -55,8 +58,19 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | var fs = require('fs'); - const author = fs.readFileSync('./author') - const issue_number = Number(fs.readFileSync('./number')); + // Both values come from a fork-triggered workflow and are therefore + // attacker-controlled. Validate them strictly before use to prevent + // Markdown/mention injection and bogus REST API filters. + const author = fs.readFileSync('./pr_artifact/author', 'utf8').trim(); + if (!/^[A-Za-z0-9-]{1,39}$/.test(author)) { + core.setFailed(`Refusing to proceed: untrusted author value '${author}' is not a valid GitHub username.`); + return; + } + const issue_number = Number(fs.readFileSync('./pr_artifact/number', 'utf8').trim()); + if (!Number.isInteger(issue_number) || issue_number <= 0) { + core.setFailed('Refusing to proceed: untrusted PR number is not a positive integer.'); + return; + } const opts = github.rest.issues.listForRepo.endpoint.merge({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index f1e3360a0..d4b4aa680 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -41,12 +41,23 @@ jobs: with: persist-credentials: false - # Scan code using project's configuration on https://semgrep.dev/manage - - uses: returntocorp/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d - with: - publishToken: ${{ secrets.SEMGREP_APP_TOKEN }} - publishDeployment: ${{ secrets.SEMGREP_DEPLOYMENT_ID }} - generateSarif: "1" + # The former returntocorp/semgrep-action is deprecated (the org was renamed + # to semgrep/*); the maintained approach is to install the CLI and invoke + # it directly. We use `semgrep scan` (not `semgrep ci`, which requires a + # login token): with no SEMGREP_APP_TOKEN configured this is exactly what + # the old action fell back to, running community rules with no token. + # SEMGREP_APP_TOKEN is still passed through so registry auth works if a + # token is ever added. + - name: Install Semgrep + run: python3 -m pip install --user semgrep + + # `semgrep scan --sarif` always exits 0 even with findings; continue-on-error + # is a safety net so the SARIF upload still runs if the scan itself errors. + - name: Run Semgrep + run: semgrep scan --config auto --sarif --output=semgrep.sarif + continue-on-error: true + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} # Upload SARIF file generated in previous step - name: Upload SARIF file