Skip to content

Harden release workflows against tag injection and credential leakage#45

Open
Mpdreamz wants to merge 1 commit intomainfrom
harden/release-workflows
Open

Harden release workflows against tag injection and credential leakage#45
Mpdreamz wants to merge 1 commit intomainfrom
harden/release-workflows

Conversation

@Mpdreamz
Copy link
Copy Markdown
Member

Summary

Security audit of the release-related workflows (create-major-tag.yml, required-labels.yml) revealed several gaps compared to the recently hardened docs-build and docs-deploy workflows. This PR brings them up to the same security standard.

Changes

create-major-tag.yml — 4 fixes

1. Strict semver tag validation (HIGH)

The workflow previously extracted a "major version" from GITHUB_REF via awk with no format validation. Since release: published fires for any release (including manually-created ones), an attacker with release-create permission could publish a release with an arbitrary tag name. The extracted value would then be force-pushed as a tag, potentially overwriting legitimate major version tags like v1 that downstream consumers depend on.

Now rejects any tag not matching ^[0-9]+\.[0-9]+\.[0-9]+$ before performing any git operations.

2. persist-credentials: false on checkout (HIGH)

The checkout step was storing contents: write-scoped credentials in .git/config, making them available to all subsequent run: steps. If any step were compromised or the workflow extended carelessly, those credentials could push arbitrary code to any branch. Now uses explicit token-based remote URL (set and immediately cleared) only in the step that needs it — matching the pattern used in changelog/submit.

3. Least-privilege permissions (MEDIUM)

contents: write was set at the workflow level, meaning any future job added to this file would silently inherit write access. Moved to contents: read at workflow level with contents: write granted only to the job that needs it.

4. Concurrency guard (MEDIUM)

Two releases published in quick succession (e.g. 2.0.0 and 2.1.0) would both extract MAJOR_VERSION=2 and race on git push -f for the v2 tag. Added a serializing concurrency group with cancel-in-progress: false so tag updates are applied in order.

required-labels.yml — 2 fixes

5. Hardened pull_request_target checkout (MEDIUM)

This workflow runs on pull_request_target (base-repo context with elevated trust). While the default checkout is the base branch (safe today), the lack of explicit constraints made it a footgun — a future edit adding ref: ${{ github.event.pull_request.head.sha }} would immediately become a critical code-execution vulnerability from fork PRs.

Now uses sparse-checkout limited to the single file it needs (.github/release-drafter.yml) and persist-credentials: false to eliminate credential exposure.

6. Delimiter-based GITHUB_OUTPUT (LOW-MEDIUM)

The yq output was written to GITHUB_OUTPUT using inline key=value format. If the parsed YAML ever produced output containing newlines, it could inject additional output variables. Now uses a random hex delimiter boundary, which is the recommended pattern for multi-line or untrusted output values.

Test plan

  • Merge a PR with a release label → required-labels check should still pass and correctly extract labels from release-drafter.yml
  • Publish a release with a valid semver tag (e.g. 99.0.0) → create-major-tag should create v99
  • Verify that publishing a release with an invalid tag (e.g. foo-bar) is rejected by the validation step

Made with Cursor

@Mpdreamz Mpdreamz added the fix label Mar 27, 2026
@Mpdreamz Mpdreamz requested a review from reakaleek March 27, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant