Skip to content

chore: stop release tag from reaching shell, sed, and manifests unvalidated - #191

Merged
spark2ignite merged 2 commits into
mainfrom
claude/autopatch-scan-935e45d5-vuln-4945883-85d5zf
Oct 1, 2026
Merged

spark2ignite merged 2 commits into
mainfrom
claude/autopatch-scan-935e45d5-vuln-4945883-85d5zf

Conversation

@spark2ignite

Copy link
Copy Markdown
Collaborator

Summary

Sync Package Manifests and Post-release Verify put the release tag (the workflow_dispatch input, or the release: published tag name) into run: scripts with ${{ }}. Expressions are filled in before bash parses the script, so a crafted tag ran as shell. In sync-manifests.yml that shell runs in the job that holds CI_PUSH_TOKEN, which can push to protected main, and WINGET_GH_PAT. The same tag also reached packaging/render.sh unchecked, which puts it into a |-delimited sed program and into the Homebrew/Scoop/WinGet manifests.

Changes

.github/workflows/sync-manifests.yml and .github/workflows/post-release-verify.yml (all four jobs)

  • The event name, dispatch input and release tag come in through env: and are read as quoted "$VAR". No caller-supplied value is pasted into a run: block any more. gh release download uses "$TAG".
  • The tag must match ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$, or the job stops before the download and before the tag is written to GITHUB_OUTPUT.

.github/workflows/sync-manifests.yml (credentials)

  • Checkout uses persist-credentials: false and no longer takes CI_PUSH_TOKEN; it fetches with the default GITHUB_TOKEN.
  • CI_PUSH_TOKEN is set only on the commit step. git receives it as an auth header through GIT_CONFIG_* env, for the git push origin HEAD:main command only, and the header is masked.
  • WINGET_GH_PAT is no longer in the clone URL, so it isn't saved in the fork clone's .git/config. It is sent the same way, only for the clone and push to the fork.

packaging/render.sh

  • It rejects a version that isn't MAJOR.MINOR.PATCH[-PRERELEASE], a base URL outside https://[A-Za-z0-9._~/-]+, and a checksum that isn't 64 lowercase hex characters. That leaves no character that can change the sed program, the Ruby formula, or the JSON/YAML manifests.
  • Checksums are looked up by exact filename (awk '$2 == f') instead of a regex grep.

Testing

  • Rendering v2.7.7 gives a formula byte-for-byte identical to the old script's output. A v2.8.0-rc.1 render passes ruby -c and parses as valid JSON and YAML.
  • All existing v* tags match the new pattern.
  • These inputs are rejected and nothing runs: v0$(touch /tmp/pwned), a tag with an embedded newline, |e … sed payloads, a " breakout into Ruby, and v1.2.3-...
  • git config --get-urlmatch confirms that the push header applies to github.com/doitintl/dci-cli and the WinGet header only to the fork, not to microsoft/winget-pkgs.
  • I haven't run either workflow on Actions. A workflow_dispatch of Sync Package Manifests with a bad tag such as v0$(touch /tmp/pwned) should fail at "Resolve release tag".

Follow-ups outside this diff

  • Narrower tokens: move CI_PUSH_TOKEN and WINGET_GH_PAT to fine-grained tokens scoped to this repo and to the winget fork.
  • Protected environment: put both secrets in a GitHub Environment limited to main and v* tags, and add environment: to render-and-sync. Without that, a branch's own edited copy of the workflow can still read the secrets.
  • Release dispatch JSON: release.yml builds the scribe dispatch JSON by hand from the tag. Build it with jq --arg instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LE9LfkthhkrwR8PFLpAvWP


Generated by Claude Code

claude added 2 commits October 1, 2026 16:04
…idated

Sync Package Manifests and Post-release Verify pasted the release tag
(dispatch input or release name) into run: scripts via ${{ }}, so a tag
such as v1.2.3$(cmd) executed in a job holding CI_PUSH_TOKEN and
WINGET_GH_PAT. Bind the tag through env:, reject anything that is not
vMAJOR.MINOR.PATCH[-PRERELEASE], and use "$TAG" in gh release download.

Stop persisting CI_PUSH_TOKEN in .git/config: check out with
persist-credentials: false and hand the PAT only to the push command.

render.sh now validates the version, base URL and checksums before
splicing them into the |-delimited sed program and the Ruby/JSON/YAML
manifests, and looks checksums up by exact filename.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LE9LfkthhkrwR8PFLpAvWP
…regex

Pass the CI_PUSH_TOKEN and WINGET_GH_PAT auth headers to git through
GIT_CONFIG_* env for only the commands that need them, instead of on the
command line or embedded in the winget-pkgs clone URL (which persisted
the PAT in /tmp/winget-pkgs/.git/config). Reject empty or dot-only
prerelease segments such as v1.2.3-.. that would only fail later as an
invalid WinGet branch name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LE9LfkthhkrwR8PFLpAvWP
@spark2ignite
spark2ignite requested review from a team, apgiorgi, chaim0m and taltultc as code owners October 1, 2026 16:07
@github-actions github-actions Bot added automation GitHub Actions and repository automation changes release Release configuration and publishing changes packaging Package manager and distribution changes labels Oct 1, 2026

@claude claude 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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline nit, I checked a couple of credential-scoping questions raised by this change and ruled them out: the CI_PUSH_TOKEN auth header is set with a broad http.https://github.com/.extraheader (no path), but it's only live for the single git push origin HEAD:main invocation against the already-checked-out repo, so it can't leak to another host in that command. I also confirmed the git fetch upstream master from microsoft/winget-pkgs runs outside the with_winget_auth wrapper, so WINGET_GH_PAT is never sent to upstream, only to the fork.

Extended reasoning...

The diff hardens CI_PUSH_TOKEN and WINGET_GH_PAT handling (header-based auth via GIT_CONFIG env vars instead of URL/argv embedding), adds strict tag/version/URL validation before values reach run: scripts, sed, and manifest templates, and tightens a checksum lookup to an exact match. This is security-sensitive CI credential and injection-prevention code touching release automation that can push to protected main and open a PR against microsoft/winget-pkgs. One confirmed nit (duplicated tag-validation regex across 5 jobs) was already posted inline; I additionally traced the scope of the two auth-header grants and the unauthenticated upstream fetch and found no leak, which is the fact recorded above.

Comment thread .github/workflows/post-release-verify.yml
@spark2ignite
spark2ignite merged commit 3b6f717 into main Oct 1, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automation GitHub Actions and repository automation changes packaging Package manager and distribution changes release Release configuration and publishing changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants