fix(ci): close shell-injection vector via head.ref in create-release.yml (BUNYIP-236) #263

Merged
YousifShkara merged 2 commits from fix/BUNYIP-236-create-release-injection into main 2026-06-29 10:41:37 +02:00
Owner

Audit finding from BUNYIP-205 roll-up (medium, exploitable, mirror of PMS-500 which already shipped on mokosh-server). The previous shape interpolated ${{ github.event.pull_request.head.ref }} directly into the nushell script body:

let branch = "${{ github.event.pull_request.head.ref }}"

Forgejo Actions expand ${{ ... }} into the script TEXT before the shell sees it, so a PR branch named release/v1.0.0"; ^rm -rf /; # would inject literal shell into nushell. The job-level if: requires the ref to start with release/v, but the smuggled-suffix branch above satisfies that check. The workflow holds contents: write plus a PAT with write:repository scope, so a successful injection could mint rogue releases, push to main, or exfiltrate the token.

Two layers of defence:

  1. Move head.ref into an env mapping (PR_BRANCH) and read $env.PR_BRANCH inside the script. The runner binds the value before shell parsing, so it is data, not code. Mirror of the PMS-500 fix.

  2. Validate $env.PR_BRANCH against strict semver ^release/v[0-9]+\.[0-9]+\.[0-9]+$ before any downstream command uses it. Belt-and-braces: every downstream use (git describe, git log, http post) now takes the value as a data arg via the env mapping, so this second check guards against a future revert undoing layer #1.

No regression to the release flow: a real release/vX.Y.Z branch passes the regex and the downstream commands run identically.

Closes the corresponding checkbox on BUNYIP-205.

#BUNYIP-236

Audit finding from BUNYIP-205 roll-up (medium, exploitable, mirror of PMS-500 which already shipped on mokosh-server). The previous shape interpolated `${{ github.event.pull_request.head.ref }}` directly into the nushell script body: let branch = "${{ github.event.pull_request.head.ref }}" Forgejo Actions expand `${{ ... }}` into the script TEXT before the shell sees it, so a PR branch named `release/v1.0.0"; ^rm -rf /; #` would inject literal shell into nushell. The job-level `if:` requires the ref to start with `release/v`, but the smuggled-suffix branch above satisfies that check. The workflow holds `contents: write` plus a PAT with write:repository scope, so a successful injection could mint rogue releases, push to main, or exfiltrate the token. Two layers of defence: 1. Move `head.ref` into an env mapping (`PR_BRANCH`) and read `$env.PR_BRANCH` inside the script. The runner binds the value before shell parsing, so it is data, not code. Mirror of the PMS-500 fix. 2. Validate `$env.PR_BRANCH` against strict semver `^release/v[0-9]+\.[0-9]+\.[0-9]+$` before any downstream command uses it. Belt-and-braces: every downstream use (git describe, git log, http post) now takes the value as a data arg via the env mapping, so this second check guards against a future revert undoing layer #1. No regression to the release flow: a real release/vX.Y.Z branch passes the regex and the downstream commands run identically. Closes the corresponding checkbox on BUNYIP-205. #BUNYIP-236
fix(ci): close shell-injection vector via head.ref in create-release.yml (BUNYIP-236)
Some checks failed
E2E / Playwright against deployment (pull_request) Successful in 33s
Check / fmt + clippy + build + tests (pull_request) Failing after 32m15s
11332b8c82
Audit finding from BUNYIP-205 roll-up (medium, exploitable, mirror of PMS-500 which already shipped on mokosh-server). The previous shape interpolated `${{ github.event.pull_request.head.ref }}` directly into the nushell script body:

    let branch = "${{ github.event.pull_request.head.ref }}"

Forgejo Actions expand `${{ ... }}` into the script TEXT before the shell sees it, so a PR branch named `release/v1.0.0"; ^rm -rf /; #` would inject literal shell into nushell. The job-level `if:` requires the ref to start with `release/v`, but the smuggled-suffix branch above satisfies that check. The workflow holds `contents: write` plus a PAT with write:repository scope, so a successful injection could mint rogue releases, push to main, or exfiltrate the token.

Two layers of defence:

1. Move `head.ref` into an env mapping (`PR_BRANCH`) and read `$env.PR_BRANCH` inside the script. The runner binds the value before shell parsing, so it is data, not code. Mirror of the PMS-500 fix.

2. Validate `$env.PR_BRANCH` against strict semver `^release/v[0-9]+\.[0-9]+\.[0-9]+$` before any downstream command uses it. Belt-and-braces: every downstream use (git describe, git log, http post) now takes the value as a data arg via the env mapping, so this second check guards against a future revert undoing layer #1.

No regression to the release flow: a real release/vX.Y.Z branch passes the regex and the downstream commands run identically.

Closes the corresponding checkbox on BUNYIP-205.

#BUNYIP-236
fix(ci): disable provenance attestation in docker check to avoid manifest-tag collision
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 1m12s
Check / fmt + clippy + build + tests (pull_request) Successful in 22m11s
Create release / Create release from merged PR (pull_request) Has been skipped
d3882ce8fe
BUNYIP-241: the `Docker builder stage` step in check.yml has been failing on PRs with `image "docker.io/library/bunyip-api-builder:check": already exists` AFTER the build itself succeeded. The cause is buildkit's default-on provenance / SBOM attestation: the exporter emits both an image manifest and an attestation manifest list and names BOTH under the requested tag, which collides inside the same run after the first `naming to ... done`. BUNYIP-231's `docker image rm` only clears cross-run leftover state and so does not address this.

The check step does not consume the resulting image (the only assertion that runs against it is the in-Dockerfile binary-size sanity check during the build itself), so the provenance attestation is not load-bearing here. `--provenance=false` collapses the export to a single manifest with no observable downside, and the BUNYIP-231 image-rm guard stays for cross-run protection.

#BUNYIP-241
YousifShkara deleted branch fix/BUNYIP-236-create-release-injection 2026-06-29 10:41:38 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!263
No description provided.