Skip to content

ci: prevent command injection in release workflow - #18

Open
dinwwwh wants to merge 1 commit into
mainfrom
claude/release-workflow-injection
Open

dinwwwh wants to merge 1 commit into
mainfrom
claude/release-workflow-injection

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 25, 2026

Copy link
Copy Markdown
Member

The release workflow substituted its workflow_dispatch inputs (version, tag) directly into shell scripts, so a crafted value could run arbitrary commands in the job that publishes to npm with provenance (id-token: write). The version check didn't help, because it ran after the substituted line had already executed. Inputs now reach the scripts only as environment variables, and both the version and the dist-tag are validated in the first step, before anything is installed, bumped or published.

Fixes

  • Crafted version or tag inputs no longer execute: no ${{ }} expression remains inside any run: script
  • Malformed or option-like dist-tags (e.g. x --registry=…) now fail the run immediately instead of reaching pnpm publish
  • Dist-tags must now start with a lowercase letter and contain only a-z, 0-9, ., _ or -

Testing

  • Ran the original and patched workflows side by side under act, with bump/changelog/publish commands replaced by argument printers. Valid inputs (stable, prerelease, explicit tag, every skip flag on/off) produce identical commands; payloads that executed on the original are rejected in the first step
  • zizmor: 3 template-injection findings → 0. actionlint and ESLint pass; the only remaining note is an intentionally unquoted $NO_GIT_CHECKS, so an empty value adds no argument

Pass workflow_dispatch inputs to shell scripts through env instead of
${{ }} substitution, and validate the npm dist-tag alongside the version
in the first step so a malformed tag fails before install or publish.
@codecov

codecov Bot commented Sep 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

This PR hardens .github/workflows/release.yaml against command injection from workflow_dispatch inputs in the job that publishes to npm with provenance.

  • Untrusted inputs moved out of scripts — version and tag now reach scripts only through env (VERSION at job level, TAG_INPUT at step level). No ${{ }} expression remains inside any run: block, so a crafted value can no longer be executed by the shell.
  • Validation reordered to run first — the version step now validates the format and exit 1s on mismatch before anything is installed, bumped or published, preserving the latest preid guard.
  • Dist-tag validation added — the resolved tag (explicit input, else preid, else latest) must match ^[a-z][a-z0-9._-]*$ before it is written to $GITHUB_OUTPUT and passed to pnpm publish --tag.
  • Boolean skip flags modernized — skip_bump / skip_changelog use typed !inputs.skip_*, and skip_git_check builds NO_GIT_CHECKS from the typed boolean; behavior is equivalent to the old github.event.inputs.* != 'true' string checks.

Verified independently: the anchored regexes reject newline, space and option-like payloads; inputs.<name> preserves boolean type for workflow_dispatch (per GitHub docs), so !inputs.skip_* is correct; and the $GITHUB_OUTPUT write is regex-gated, so no output-key injection. The remaining unquoted $NO_GIT_CHECKS is safe as documented in the PR description (empty expansion adds no argument).

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant