Skip to content

fix(ci): update-homebrew keeps run metadata out of scripts - #16140

Merged
lawrencecchen merged 2 commits into
mainfrom
fix-homebrew-workflow-run-input
Sep 30, 2026
Merged

lawrencecchen merged 2 commits into
mainfrom
fix-homebrew-workflow-run-input

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

update-homebrew.yml now passes the triggering run's branch and the dispatch input to its version step through env: instead of substituting them into the script, and its gate accepts only a .github/workflows/release.yml run from this repository triggered by a push (manual dispatch unchanged).

Found by a Codex Security audit (both models agreed). Commit 1 adds the failing test (it renders the step the way the runner does, with a hostile branch name, and checks nothing executes), commit 2 the fix.

Verification: python3 tests/test_ci_homebrew_untrusted_input.py, tests/test_release_homebrew_gate.py, tests/test_ci_workflow_run_sources.py, tests/test_ci_workflow_guards_are_wired.py; actionlint reports only the four pre-existing SC2086 infos.

Changelog

none

🤖 Generated with Claude Code


Summary by cubic

Fixes a command injection vulnerability in the Homebrew cask update workflow where a hostile branch name from a triggering run could execute arbitrary code with the tap token.

  • Run metadata and the dispatch input now reach the version script through env: instead of being substituted into script text, so values like $(cmd) can't run.
  • The workflow_run gate now accepts only the real release.yml run from this repository triggered by a push; manual dispatch is unchanged.
  • Adds a test that renders the version step the way the runner does with a hostile branch name and confirms nothing executes.

Written for commit f3b70ce. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Homebrew updates now run only for eligible release push events or manual dispatches. Event data is handled safely, and untrusted branch values cannot alter workflow execution.
  • Tests
    • Added checks for Homebrew update eligibility and safe handling of event data.

lawrencecchen and others added 2 commits September 30, 2026 11:22
…ripts

update-homebrew.yml substitutes github.event.workflow_run.head_branch and
the dispatch input into its `run:` script as text. The test renders the
version step the way the runner does, with a hostile branch name, and
checks nothing executes; it also requires the gate to accept only the
real release workflow run from a tag push in this repository. Found by a
Codex Security audit (both models).

Red: python3 tests/test_ci_homebrew_untrusted_input.py

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ly the real release run

Makes tests/test_ci_homebrew_untrusted_input.py green (previous commit).

Root cause: the version step substituted
github.event.workflow_run.head_branch and the dispatch input into its
script text, and the gate accepted any run of a workflow with the
release workflow's display name. The values now reach the script as
env vars (INPUT_VERSION, HEAD_BRANCH, EVENT_NAME), and the gate requires
the run to be .github/workflows/release.yml, from this repository, for a
push. Dispatch keeps working as before.

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

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 5afde9a8-3901-4636-8a5f-41a06e2a8876

📥 Commits

Reviewing files that changed from the base of the PR and between e6662a5 and f3b70ce.

📒 Files selected for processing (4)
  • .github/workflows/ci-guards.yml
  • .github/workflows/update-homebrew.yml
  • tests/test-execution.toml
  • tests/test_ci_homebrew_untrusted_input.py

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The Homebrew update workflow filters workflow_run events and passes event values to its version step through environment variables. A new test checks the workflow gate and hostile branch handling, and CI runs the test.

Changes

Homebrew workflow safeguards

Layer / File(s) Summary
Filter events and handle version inputs
.github/workflows/update-homebrew.yml
The gate accepts qualifying release pushes and manual dispatches. The version step reads event values from environment variables and keeps the existing version selection and validation.
Test and run the input safeguards
tests/test_ci_homebrew_untrusted_input.py, tests/test-execution.toml, .github/workflows/ci-guards.yml
The test checks hostile branch handling, expression interpolation, and gate conditions. The test registry and release-notary workflow job run the test.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Bug fix

Suggested reviewers: teamleaderleo

Merge Risk: ⚪ Minimal · up to f3b70

The workflow tightens release-run eligibility and safely passes event inputs through environment variables, with safeguard tests registered in CI. No actionable merge-blocking risk remains after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f3b70

The change narrows which release runs can update Homebrew and handles event values as data rather than executable shell text. No introduced security regression was identified. The publishing job remains privileged, and its live credential scope and runner isolation were not verified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The security-sensitive outcome is publication of Casks/cmux.rb to the external Homebrew tap, whose consumers inherit its version and download checksum. The configured write target is one repository, but the maximum authority of HOMEBREW_TAP_TOKEN cannot be established from its secret reference. The PR neither changes that credential nor adds a publication target.

Security Findings and Attack Paths

  • inferred — The base version step inserted event values into shell source before parsing, allowing shell metacharacters in reachable input to become commands. At head, quoted variable expansion treats those values as data, and numeric version validation prevents hostile values from reaching downstream version-dependent publication steps. Source comparison supports removal of this path rather than an introduced security concern.

Trust Boundaries and Controls

  • observed — The workflow now checks source workflow path, repository identity, and push-event type before accepting workflow-run authority. A successful signed-build job remains required afterward. Manual dispatch remains an explicit operator assertion rather than gaining a new bypass through this PR.

Resilience and Maintainability Implications

  • observed — The new test executes only the rendered version step with a hostile branch value, checks marker absence and skip output, rejects direct event interpolation in scripts, and checks gate-condition substrings. It does not exercise the gate API lookup or the download-to-push lifecycle. Its subprocess receives an explicitly constructed environment rather than inheriting the complete test-process environment; it is not a sandbox.
🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (3 skipped: 3… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: keeping run metadata out of scripts in the Homebrew workflow.
Description check ✅ Passed The description explains the security problem, resulting behavior, implementation, tests executed, and changelog status. The omitted demo video and checklist are not critical for this CI-only change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Cloud Persistent Session And Early Input ✅ Passed PASS: The pull request changes only Homebrew workflow gating, CI test registration, and a Homebrew metadata-safety test. It does not change cloud terminal creation, cmux-tui client or physical transpo…
Cmux Swift Actor Isolation ✅ Passed The pull request changes only YAML, TOML, and Python files. The authoritative diff contains no Swift files or production Swift changes, so the Swift actor isolation check is not applicable.
Cmux Swift Blocking Runtime ✅ Passed The pull request changes only GitHub workflow YAML, a TOML test registry, and a Python test file. It contains no Swift changes, so the Swift blocking-runtime check is not applicable.
Cmux Browser Automation Off-Main ✅ Passed PASS: The pull request changes only GitHub workflow and test files. It does not change the rule-scoped Swift browser automation files, worker routing, WebKit/AppKit access, or policy tests. The change…
Cmux Expensive Synchronous Load ✅ Passed PASS: The pull request changes only two YAML workflows, one TOML registry, and one Python test. It adds no production Swift files or Swift call sites, so it cannot introduce an expensive synchronous a…
Cmux Cache Substitution Correctness ✅ Passed PASS: The pull request changes only GitHub Actions YAML, a TOML test registry, and a Python regression test. The authoritative diff contains no production Swift, TypeScript, or JavaScript changes, and…
Cmux No Hacky Sleeps ✅ Passed PASS. The pull request changes workflow YAML, TOML test registration, and a Python test only. It adds no sleep, timer, polling, or fixed-delay code. The existing sleep 30 in `.github/workflows/updat…
Cmux Algorithmic Complexity ✅ Passed The pull request does not introduce an algorithmic-complexity violation. The production workflow changes only gate conditions and environment-variable handling. Its existing download loop has the expl…
Cmux Swift Concurrency ✅ Passed The pull request changes only GitHub Actions workflows and Python/TOML test files. The authoritative diff contains no Swift code or Swift concurrency changes, so the custom check is not applicable.
Cmux Swift @Concurrent ✅ Passed The pull request changes only YAML, TOML, and Python files. It contains no Swift changes, so the Swift @concurrent check is not applicable.
Cmux Swift Package Boundaries ✅ Passed PASS: The reviewed diff changes only two GitHub workflow YAML files, one TOML registry, and one Python test. It contains no production Swift files or Swift package changes, so the Swift package-bounda…
Cmux Swiftpm Lockfiles ✅ Passed PASS: The authoritative PR diff changes only two GitHub workflow files and two Python/TOML test files. It contains no SwiftPM package, Xcode project, .gitignore, dependency manifest, or `Package.res…
Cmux Swift Logging ✅ Passed PASS: The pull request changes only YAML, TOML, and Python files. It adds no Swift code or production logging. The added Python print calls are test diagnostics, which the check excludes.
Cmux User-Facing Error Privacy ✅ Passed PASS. The PR changes GitHub Actions gating and shell-input handling, plus CI regression tests. It does not add or materially change cmux app UI, product CLI, or product API error text. The workflow me…
Cmux Full Internationalization ✅ Passed PASS: The PR changes GitHub Actions workflow logic, CI test registration, and a developer-only regression test. It introduces no Swift UI text, string-catalog or Info.plist entries, web UI/API copy, m…
Cmux Swiftui State Layout ✅ Passed PASS: The PR changes only GitHub Actions YAML, TOML, and Python test files. The authoritative diff contains no Swift or SwiftUI changes, so it introduces none of the state, layout, row-store, or rende…
Cmux Architecture Rethink ✅ Passed PASS: The pull request changes only GitHub workflow YAML, Python tests, and TOML. The authoritative diff contains no Swift files or Swift architecture changes. The custom check is therefore not applic…
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The pull request changes only GitHub workflow files and Python/TOML test files. It contains no Swift changes and does not add or modify any cmux-owned auxiliary window.
Cmux Source Artifacts ✅ Passed All four changed paths are intentional repository sources: two GitHub workflow configs, the test execution registry, and a hand-written Python regression test. The diff adds no logs, screenshots, reco…
Cmux No Test Or Debug Seam In Production Source ✅ Passed The pull request changes only workflow files and Python/TOML test configuration. The authoritative diff contains no Swift file under a production Sources/ path, so it cannot introduce a prohibited t…
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lawrencecchen
lawrencecchen merged commit 55c3817 into main Sep 30, 2026
56 checks passed
@lawrencecchen
lawrencecchen deleted the fix-homebrew-workflow-run-input branch September 30, 2026 19:06
@github-actions

Copy link
Copy Markdown
Contributor

Merge receipt for f3b70ce478: every check was green at merge (10 verified; 13 skipped by policy). Full suite runs on main after merge.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review, after the fact: a subagent reviewed f3b70ce478b on its own diff before this merged. Verdict was safe to merge, so nothing here should have held it. Posting the findings anyway because two of them are now on main, and both are one-line fixes: I have put them up as #16197.

The injection is closed. I enumerated every ${{ }} inside every run: body on that revision by parsing the YAML rather than reading the diff. Exactly one remains, steps.version.outputs.version at line 128, and it is quoted and regex-constrained. All three formerly-interpolated event values arrive through env: and are quoted at every use site. I rendered and executed the version step against ten hostile branch names ($(touch${IFS}X), backticks, v1.2.3;touch X, v1.2.*, -rf, an embedded newline, v${HOME}.2.3 and others): every one produced skip=true, no command ran, and none forged a GITHUB_OUTPUT key. Only v1.2.3 yielded a version.

The guard has teeth. Reverting only the workflow hunks and keeping the new test gives 8 failures, and the first one is the marker file, so it proves execution and not a text pattern. Each half reverted independently also fails: env:-only revert 5 failures, gate-only revert 3. The test is registered in tests/test-execution.toml:1516-1519, invoked in the release-notary group, and it ran and passed in CI on this head (run 36758172526). update-homebrew.yml routes fail-open to all twelve guard groups, so it fires on a future regression too.

Two findings, both medium, both now in #16197:

  1. event == 'push' blocks the dispatch recovery path. release.yml triggers on a v* tag push and on workflow_dispatch, and 20 of the last 30 Release macOS app runs were dispatches. v0.64.23, v0.64.24 and v0.64.25 all had a failing tag-push run in September; dispatching on the tag is how the cask gets shipped after that. With this gate the job skips silently and the tap stays behind, which is the failure the comment at lines 32-39 describes. Re-running the original push run is unaffected; it is the dispatch path that regressed.
  2. The guard does not pin the version regex, so line 128's safety is undefended. Change line 111 to ^[0-9]+\.[0-9]+\.[0-9]+.*$ for prerelease tags and the guard still reports all six checks ok, while a branch named v1.2.3$(touch${IFS}PWNED) executes in the job holding the tap token. I confirmed that (PWNED created: True). Reaching it needs write access, so it is a durability gap rather than a live hole.

Two low ones, also in #16197: the sweep's prefix list misses inputs.version, the other spelling GitHub exposes for a dispatch input (I interpolated ${{ inputs.version }} straight into a run: body and the guard passed), and the new comment at lines 92-94 lost a sentence boundary so it now reads as the inverse of what it means.

One informational note, pre-existing and not touched here: the cask heredoc at line 163 uses an unquoted delimiter, so its body takes command substitution. Not exploitable while VERSION is regex-constrained and SHA256 is hex, but it makes a future loosening of either into code execution rather than a malformed cask.

The one thing I could not settle, and it is yours to answer rather than mine: whether any of those 20 dispatches was meant to publish a cask, or whether dispatching update-homebrew.yml by hand is already how you recover. If it is the latter, finding 1 is cosmetic and #16197 is only tidying. If not, #16197 restores a path you use.

Fixed: nothing in this PR (it merged first). Left: findings 1 through 4, all in #16197.

— Raindrop g2 🫧

teamleaderleo added a commit that referenced this pull request Sep 30, 2026
* test(ci): pin the homebrew gate's trigger allow-list and version pattern

#16140 closed the injection but left three ways to reopen it or to lose a
release. The guard did not assert the version regex that keeps the one
remaining interpolation safe, did not know the `inputs.version` spelling of a
dispatch input, and did not say which triggers the gate may accept.

Add, all failing against the current workflow:
- the gate allow-lists `github.event.workflow_run.event` and admits exactly
  push and workflow_dispatch, so neither a widening nor a narrowing is silent
- `steps.version.outputs` reaches a script through `env:` like the rest of the
  attacker-derived values
- the version step pins an anchored semver pattern with no wildcard tail
- a semver-prefixed payload (`v1.2.3$(...)`), which a bare `$(...)` payload
  cannot stand in for once the pattern is widened
- `inputs.` as an interpolation prefix

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

* fix(ci): let the homebrew gate accept a dispatched release run

release.yml ships from two triggers: `push: tags: v*` and `workflow_dispatch`
(.github/workflows/release.yml:3-7). #16140's gate accepted only `push`, which
removes the recovery path the file's own comment was added for. Of the last 30
Release macOS app runs, 20 were dispatches; v0.64.23, v0.64.24 and v0.64.25 all
had a failing tag-push run in September, and the dispatch is how a maintainer
publishes the cask after one of those. With the gate as written the tap would
silently stay on the previous version, which is the exact failure the comment at
lines 32-39 describes.

Allow-list both triggers with the idiom already used at
ci-compile-attribution.yml:55. Dispatching release.yml needs write access and
`head_repository.full_name == github.repository` still holds, so this does not
widen the trust boundary; `pull_request` stays out.

Also move the last interpolation out of a `run:` body. `steps.version.outputs.version`
is the branch name minus a `v`, admitted by an anchored semver regex. Safe
today, but its safety lives in a regex three steps away: widen that pattern to
take `1.2.3-beta.1` and a branch named `v1.2.3$(cmd)` runs in the job holding
HOMEBREW_TAP_TOKEN. Passing it through `env:` removes the dependency.

Fixes the comment at lines 92-94, which lost a sentence boundary and reads as
the opposite of what it means.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Sep 30, 2026
* test(ci): update-homebrew must keep triggering-run metadata out of scripts

update-homebrew.yml substitutes github.event.workflow_run.head_branch and
the dispatch input into its `run:` script as text. The test renders the
version step the way the runner does, with a hostile branch name, and
checks nothing executes; it also requires the gate to accept only the
real release workflow run from a tag push in this repository. Found by a
Codex Security audit (both models).

Red: python3 tests/test_ci_homebrew_untrusted_input.py

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

* fix(ci): update-homebrew takes run metadata through env and trusts only the real release run

Makes tests/test_ci_homebrew_untrusted_input.py green (previous commit).

Root cause: the version step substituted
github.event.workflow_run.head_branch and the dispatch input into its
script text, and the gate accepted any run of a workflow with the
release workflow's display name. The values now reach the script as
env vars (INPUT_VERSION, HEAD_BRANCH, EVENT_NAME), and the gate requires
the run to be .github/workflows/release.yml, from this repository, for a
push. Dispatch keeps working as before.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 55c3817)
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.

2 participants