Skip to content

Supply chain: install.sh checksum verification fails open, and the workflow_run chain is triggerable by a fork PR #10

Description

@catinspace-au

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions