From c18ae9b40c3c6eea9f24752a3397228d28aff309 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 27 Mar 2026 15:29:10 +0100 Subject: [PATCH 1/2] Harden release workflows against tag injection and credential leakage Made-with: Cursor --- .github/workflows/create-major-tag.yml | 27 ++++++++++++++++++++++---- .github/workflows/required-labels.yml | 11 +++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.github/workflows/create-major-tag.yml b/.github/workflows/create-major-tag.yml index 4854188..85d831d 100644 --- a/.github/workflows/create-major-tag.yml +++ b/.github/workflows/create-major-tag.yml @@ -6,18 +6,37 @@ on: - published permissions: - contents: write + contents: read + +concurrency: + group: create-major-tag + cancel-in-progress: false jobs: create-major-tag: + permissions: + contents: write runs-on: ubuntu-slim steps: - uses: actions/checkout@v6 - - name: Get major version + with: + persist-credentials: false + + - name: Validate and extract major version + env: + TAG: ${{ github.event.release.tag_name }} run: | - MAJOR_VERSION=$(echo "${GITHUB_REF#refs/tags/}" | awk -F. '{print $1}') - echo "MAJOR_VERSION=${MAJOR_VERSION}" >> "${GITHUB_ENV}" + if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Tag '${TAG}' does not match expected semver format (N.N.N)" + exit 1 + fi + echo "MAJOR_VERSION=${TAG%%.*}" >> "${GITHUB_ENV}" + - name: Create major tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" git tag "v${MAJOR_VERSION}" git push -f origin "refs/tags/v${MAJOR_VERSION}" + git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git" diff --git a/.github/workflows/required-labels.yml b/.github/workflows/required-labels.yml index 560b140..f1a252d 100644 --- a/.github/workflows/required-labels.yml +++ b/.github/workflows/required-labels.yml @@ -19,13 +19,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + sparse-checkout: .github/release-drafter.yml + persist-credentials: false - name: Wait for PR to be ready (if just opened) if: github.event_name == 'pull_request_target' && github.event.action == 'opened' run: sleep 30 - id: get-labels run: | - labels=$(yq '[.categories[].labels] + .exclude-labels | flatten | unique | sort | @tsv' .github/release-drafter.yml | tr '\t' ',') - echo "labels=$labels" >> "${GITHUB_OUTPUT}" + delimiter="$(openssl rand -hex 16)" + { + echo "labels<<${delimiter}" + yq '[.categories[].labels] + .exclude-labels | flatten | unique | sort | @tsv' .github/release-drafter.yml | tr '\t' ',' + echo "${delimiter}" + } >> "${GITHUB_OUTPUT}" - id: check-labels uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5 with: From 5a86a24c83b771f306e5e5b45325bb7f5d1d8146 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 30 Mar 2026 21:21:42 +0200 Subject: [PATCH 2/2] Set workflow-level permissions to {} for least-privilege The job already declares contents: write, so the workflow-level contents: read was redundant. Empty permissions at workflow level ensures no implicit grants. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .github/workflows/create-major-tag.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/create-major-tag.yml b/.github/workflows/create-major-tag.yml index 85d831d..c2ecaf5 100644 --- a/.github/workflows/create-major-tag.yml +++ b/.github/workflows/create-major-tag.yml @@ -5,8 +5,7 @@ on: types: - published -permissions: - contents: read +permissions: {} concurrency: group: create-major-tag