From 44467101ee591b28593e4a283384dbd09f87c30b Mon Sep 17 00:00:00 2001 From: andyne13 Date: Fri, 24 Jul 2026 09:57:03 +0200 Subject: [PATCH] =?UTF-8?q?fix(ci):=20harden=20GA=20publish=20guard=20?= =?UTF-8?q?=E2=80=94=20exact=20tag=20format,=20no=20shell=20interpolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on the verify-tag gate added in #764 (raised by @hedhoud and CodeRabbit/zizmor), all three confirmed against the merged workflow: 1. Tag format was too loose. The guard only rejected '-rc.', so v2.0.1-rc1, v2.0.1-beta, vfoo etc. passed and would publish GA images and move :latest. The rc1 case is the sharp one: build_rc.yml triggers on 'v*-rc.*' which requires the dot, so a one-character typo matched neither workflow's intent. Now validated against ^v[0-9]+\.[0-9]+\.[0-9]+$ and failed loud. 2. Template injection. ${{ github.ref_name }} expanded into the run body before the shell ran, and git permits ; $ ` " | & in ref names — arbitrary code execution in a job that holds packages:write and Docker Hub credentials. Tag name and SHA now passed via env: and referenced as shell variables. 3. Checkout persisted credentials, inconsistent with build_rc.yml which already sets persist-credentials: false on all three checkouts (95fd86fc). The repo is public, so the origin/main fetch still works without them. Behavior: vX.Y.Z on main builds; vX.Y.Z-rc.N skips to build_rc.yml; malformed or prerelease tags and off-main tags now fail loudly instead of publishing. --- .github/workflows/build.yml | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 22af842f1..8d410f66e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,13 +26,29 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Fail if the tagged commit is not on main + persist-credentials: false + # Tag name and SHA are passed as env data, never interpolated into the + # script body: ${{ }} expands before the shell runs, and git permits + # ; $ ` " | & in ref names — so a crafted tag could otherwise execute + # arbitrary code in this privileged publishing workflow. + - name: Verify this is an exact GA release tag on main + env: + TAG_NAME: ${{ github.ref_name }} + TAG_SHA: ${{ github.sha }} run: | + # The job-level `if` only filters well-formed `-rc.` tags (owned by + # build_rc.yml). Anything else reaching here must be an exact GA tag: + # a near-miss like `v1.2.3-rc1` matches neither workflow's intent and + # would otherwise publish a prerelease as GA and move `latest`. + if ! printf '%s' "$TAG_NAME" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::$TAG_NAME is not an exact GA release tag (vMAJOR.MINOR.PATCH) — refusing to publish GA images." + exit 1 + fi git fetch --no-tags origin main - if git merge-base --is-ancestor "${{ github.sha }}" FETCH_HEAD; then - echo "OK: ${{ github.ref_name }} (${{ github.sha }}) is on main" + if git merge-base --is-ancestor "$TAG_SHA" FETCH_HEAD; then + echo "OK: $TAG_NAME ($TAG_SHA) is an exact GA tag on main" else - echo "::error::Tag ${{ github.ref_name }} is not on main — refusing to build GA images." + echo "::error::$TAG_NAME is not on main — refusing to publish GA images." exit 1 fi