Skip to content

Harden release workflows against tag injection and credential leakage#45

Merged
Mpdreamz merged 2 commits intomainfrom
harden/release-workflows
Apr 7, 2026
Merged

Harden release workflows against tag injection and credential leakage#45
Mpdreamz merged 2 commits 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
Copy link
Copy Markdown
Contributor

@cotti cotti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the job gives write, we can tight the workflow-level perms a bit more. Otherwise looks good!

Comment thread .github/workflows/create-major-tag.yml Outdated
Comment on lines +8 to +9
permissions:
contents: write
contents: read
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
permissions:
contents: write
contents: read
permissions: {}

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) <noreply@anthropic.com>
@Mpdreamz
Copy link
Copy Markdown
Member Author

Thanks @cotti! Good catch — applied the suggestion. Setting permissions: {} at the workflow level is the right call since the job already declares what it needs.

@Mpdreamz Mpdreamz requested a review from cotti March 30, 2026 19:30
@Mpdreamz Mpdreamz merged commit 32f8296 into main Apr 7, 2026
3 checks passed
@Mpdreamz Mpdreamz deleted the harden/release-workflows branch April 7, 2026 11:24
cotti pushed a commit that referenced this pull request Apr 7, 2026
…#45)

* Harden release workflows against tag injection and credential leakage

Made-with: Cursor

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
cotti pushed a commit that referenced this pull request Apr 7, 2026
…#45)

* Harden release workflows against tag injection and credential leakage

Made-with: Cursor

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
cotti added a commit that referenced this pull request Apr 7, 2026
* Add S3 upload support for changelogs

* Add README

* Use js-yaml for path checking.

* Add pr-number parameter and tighten workflow-level permissions

* Fix IFS, add --no-follow-symlinks

* Adjust call

* Use docs-builder to upload to S3

* Update .gitignore

* Update README

* Harden release workflows against tag injection and credential leakage (#45)

* Harden release workflows against tag injection and credential leakage

Made-with: Cursor

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Adjust call

* Adjust inputs and concurrency for upload workflow

* Adjust concurrency group

* Adjust docs, improve push target pattern

* Use the push SHA instead of the merge commit SHA

* Update README

* Add config input

* Set cancel-in-progress

* Introduce config input and additional documentation

* Update README

---------

Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
cotti added a commit that referenced this pull request Apr 22, 2026
* Describe skip label availability and add skip-specific comment

* Let docs-builder know it's running on CI to set landing-page-path (#54)

* Let docs-builder know it's running on CI to set landing-page-path

* Set CI again.

* Add agentic workflow infrastructure and docs-check workflow (#56)

* Add agentic workflow infrastructure and docs-check workflow

Introduces gh-aw agentic workflow support alongside the existing composite
actions. Workflow .md sources live in workflows/ as a library; a compile
script copies them into .github/workflows/ for the gh-aw compiler and
produces .lock.yml files that consumer repos reference via workflow_call.

First workflow: docs-check — analyzes PRs/commits for documentation impact
using the Elastic Docs MCP server and posts structured findings as comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Simplify tooling: use gh extension instead of Go binary

Replace go install with gh extension install for the gh-aw compiler.
Drop actionlint binary download — the repo already has it via pre-commit.
Remove bin/ dependency entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix CI failures and address review feedback

- Move `roles` under `on:` (latest gh-aw schema change)
- Exclude .lock.yml from pre-commit trailing-whitespace and end-of-file-fixer
- Replace custom lint/release Makefile targets with `pre-commit run --all`
- Remove release target (release-drafter handles releases)
- Recompile lock file with latest gh-aw

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix CI: recompile with latest gh-aw, exclude lock files from actionlint

- Update gh-aw extension (v0.63.0 → v0.65.5) and recompile lock file
- Exclude .lock.yml from actionlint pre-commit hook (generated files
  reference secrets injected by the gh-aw runtime)
- Fix .gitattributes missing trailing newline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Rename workflows/ to agentic-workflows/

Update all references in compile script, Makefile, CI, README,
DEVELOPING.md, and workflow docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add /create-agentic-workflow Claude skill

Interactive skill that scaffolds the three required files for a new
gh-aw agentic workflow: source .md, example.yml trigger, and README.
Guides users through pattern selection, trigger config, and prompt
structure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Teach skill about fragment selection and creation

Add Step 3 that reads available fragments, explains what each provides
and when to include it, documents import rules, and guides users on
when to create new fragments vs. reuse existing ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Harden release workflows against tag injection and credential leakage (#45)

* Harden release workflows against tag injection and credential leakage

Made-with: Cursor

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Remove post-skipped-comment

* Update aw lock

---------

Co-authored-by: Fabrizio Ferri-Benedetti <fabri.ferribenedetti@elastic.co>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>
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.

2 participants