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 <mail@nlohmann.me>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 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 <mail@nlohmann.me>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Niels Lohmann
2026-07-04 11:10:02 +02:00
committed by GitHub
parent c363dc3e4d
commit 899cf31255
2 changed files with 34 additions and 9 deletions
@@ -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,