Found by an adversarial security review during the Rust-migration wrap-up. All pre-existing. The tag-injection and workflow-permissions findings from the same review were fixed inline; these three need a design decision, so they are filed.
1. install.sh checksum verification fails open (three ways)
packaging/templates/install.sh verifies only if everything lines up, and skips silently otherwise:
if curl -fsSL "$SUMS_URL" ... 2>/dev/null; then -- a 404 or any network hiccup on the sums file skips verification and installs the binary anyway.
if grep -q "$ASSET" ... -- a sums file that omits this asset skips verification. This is live today: latest/checksums.sha256 contains exactly one line, for macbash-linux-amd64, so every macOS install is unverified -- even though latest/macbash-darwin-arm64.sha256 exists and would have served.
- if neither
shasum nor openssl is present, the comparison is skipped.
SUMS_URL is also the same origin as the binary, so it is not an independent trust anchor: whoever can serve the binary can serve or withhold the sums file. There is no signature and no key pinning anywhere in the chain.
Blast radius: action.yml pipes this script into sh in every consumer repository. A compromise of the R2 bucket or the CDN is arbitrary code execution in every consumer's CI job, with whatever permissions and secrets that workflow holds -- and the non---user path does sudo mv.
Suggested: fall back to the per-asset <binary>-<os>-<arch>.sha256 that the release already publishes; fail closed when no checksum can be obtained; consider signing (minisign/cosign) with the key distributed out of band.
2. The workflow_run chain has no branch filter
cross-build.yml and package.yml gate only on github.event.workflow_run.conclusion == 'success'. CI runs on pull_request, including fork PRs. workflow_run jobs execute in the base-repo context with full secrets regardless of what triggered the upstream run.
So any external contributor whose PR makes CI pass can trigger the privileged publish pipeline: R2 writes, GitHub App token minting, and tap/bucket pushes. It does not check out untrusted PR code, so this is not code injection -- but it is an unauthenticated trigger for jobs holding production credentials.
Suggested: gate on github.event.workflow_run.head_branch == github.event.repository.default_branch and github.event.workflow_run.event != 'pull_request'.
3. Builds are not reproducible, and re-runs overwrite published artefacts
build.rs stamps SystemTime::now() into VERGEN_BUILD_TIMESTAMP. Verified: identical source and identical git SHA, two consecutive cargo build --release produce different sha256.
cross-build.yml uploads to the versioned path as well as latest/, so a re-run for an already-published tag replaces artefacts at an immutable-looking URL with different bytes. packaging/templates-release/homebrew/formula.rb pins sha256 against that versioned URL, so a re-run breaks brew install until the manifests job re-renders and pushes. Combined with finding 2, an external contributor can open that window.
The comment at cross-build.yml claiming "same source -> same bytes overwrite the same R2 paths" is false and should go either way.
Suggested: use SOURCE_DATE_EPOCH (or the tag's commit date) instead of wall-clock time, and refuse to overwrite an existing versioned path.
Found by an adversarial security review during the Rust-migration wrap-up. All pre-existing. The tag-injection and workflow-permissions findings from the same review were fixed inline; these three need a design decision, so they are filed.
1.
install.shchecksum verification fails open (three ways)packaging/templates/install.shverifies only if everything lines up, and skips silently otherwise:if curl -fsSL "$SUMS_URL" ... 2>/dev/null; then-- a 404 or any network hiccup on the sums file skips verification and installs the binary anyway.if grep -q "$ASSET" ...-- a sums file that omits this asset skips verification. This is live today:latest/checksums.sha256contains exactly one line, formacbash-linux-amd64, so every macOS install is unverified -- even thoughlatest/macbash-darwin-arm64.sha256exists and would have served.shasumnoropensslis present, the comparison is skipped.SUMS_URLis also the same origin as the binary, so it is not an independent trust anchor: whoever can serve the binary can serve or withhold the sums file. There is no signature and no key pinning anywhere in the chain.Blast radius:
action.ymlpipes this script intoshin every consumer repository. A compromise of the R2 bucket or the CDN is arbitrary code execution in every consumer's CI job, with whatever permissions and secrets that workflow holds -- and the non---userpath doessudo mv.Suggested: fall back to the per-asset
<binary>-<os>-<arch>.sha256that the release already publishes; fail closed when no checksum can be obtained; consider signing (minisign/cosign) with the key distributed out of band.2. The workflow_run chain has no branch filter
cross-build.ymlandpackage.ymlgate only ongithub.event.workflow_run.conclusion == 'success'. CI runs onpull_request, including fork PRs.workflow_runjobs execute in the base-repo context with full secrets regardless of what triggered the upstream run.So any external contributor whose PR makes CI pass can trigger the privileged publish pipeline: R2 writes, GitHub App token minting, and tap/bucket pushes. It does not check out untrusted PR code, so this is not code injection -- but it is an unauthenticated trigger for jobs holding production credentials.
Suggested: gate on
github.event.workflow_run.head_branch == github.event.repository.default_branchandgithub.event.workflow_run.event != 'pull_request'.3. Builds are not reproducible, and re-runs overwrite published artefacts
build.rsstampsSystemTime::now()intoVERGEN_BUILD_TIMESTAMP. Verified: identical source and identical git SHA, two consecutivecargo build --releaseproduce different sha256.cross-build.ymluploads to the versioned path as well aslatest/, so a re-run for an already-published tag replaces artefacts at an immutable-looking URL with different bytes.packaging/templates-release/homebrew/formula.rbpins sha256 against that versioned URL, so a re-run breaksbrew installuntil the manifests job re-renders and pushes. Combined with finding 2, an external contributor can open that window.The comment at
cross-build.ymlclaiming "same source -> same bytes overwrite the same R2 paths" is false and should go either way.Suggested: use
SOURCE_DATE_EPOCH(or the tag's commit date) instead of wall-clock time, and refuse to overwrite an existing versioned path.