OrbitalReg Sign in →

Supply-chain case study · retrospective

CodeCov bash-uploader · April 2021

The case against curl | bash — pinning, not piping.

January 2021: an attacker quietly modified the CodeCov bash-uploader script that thousands of CI pipelines fetched via curl ... | bash. Two months of CI runs exfiltrated environment variables to attacker infrastructure before a single careful customer noticed the script's SHA didn't match. Twilio, HashiCorp, Confluent, Rapid7, Mercari all confirmed impact.

What happened

A one-line CI fetch with no integrity check.

  1. 01. The standard install instruction. CodeCov's documentation recommended bash <(curl -s https://codecov.io/bash) as the way to upload coverage from CI. Thousands of pipelines wrote that line verbatim into their workflows.
  2. 02. Quiet modification. On 31 January 2021 the attacker — using credentials extracted from a flawed Docker image build process — modified the bash-uploader to add a one-liner that posted the CI runner's environment variables to attacker infrastructure. The CodeCov pages serving the script kept their existing URL and existing TLS certificate. Nothing visibly changed.
  3. 03. Two months undetected. Every CI run from late January through early April pulled the modified script and ran it in an environment carrying secrets — AWS keys, GitHub PATs, deploy keys, signing credentials, database passwords. The exfiltration was logged on the attacker side; nobody on the customer side was looking at the script's content.
  4. 04. Found by a customer doing the right thing. 1 April 2021: a customer noticed the script's SHA-256 didn't match the SHA published on the CodeCov documentation page. They reported it. CodeCov confirmed the compromise. Disclosure on 15 April. The impacted window was 31 January → 1 April — roughly two months.
The critical property: nothing in curl | bash verifies that the script you receive is the script the vendor published. The vendor's web server can serve anything it likes, signed with a perfectly valid TLS cert, on the same URL it served the right script from yesterday. There is no out-of-band integrity check. There is no version pinning. There is no audit log.

The structural gap

Vendor-served scripts are an implicit trust relationship.

Every CI pipeline that fetches a script at run-time from a vendor URL accepts the same trust model: "whatever this URL serves today, I will execute in an environment carrying my secrets." That model works exactly as well as the vendor's own internal controls — which, as CodeCov demonstrated, is not always well enough.

The fix is content-addressable instead of URL-addressable. Once the script's SHA is pinned in your CI config, a modification on the vendor's side becomes a hash-mismatch error, not a silent execution. Pinning the SHA is free; the only friction is "where does the SHA live, and how do I keep it fresh when the vendor releases a legitimate update."

That's the gap OrbitalReg's generic / raw format closes. The script is mirrored as a registry artifact with a deterministic content hash; CI fetches by hash, not by URL; legitimate updates flow through the normal promotion / approval workflow instead of bypassing it.

The OrbitalReg defense pattern

Three habits that make curl | bash a non-issue.

01

Mirror as generic artifact

OrbitalReg's generic format stores arbitrary blobs (shell scripts, signed tarballs, installer binaries) with content-addressable SHA-256 storage. The bash-uploader is fetched from upstream once, hashed, and served by hash from then on. The vendor changing the script doesn't change what your CI receives until the new hash is promoted.

02

SHA-pinned fetch in CI

CI pipelines reference the artifact by SHA, not by tag: orbital fetch generic/codecov-uploader/sha256:abc…. Any change on the vendor's side produces a different hash, and your CI's fetch fails until a human runs a promotion. The compromise window collapses from "two months" to "until the next promotion request."

03

Promotion gate on update

A scheduled "refresh from upstream" job pulls the latest script into a staging repo. A human reviewer (or an automated diff-and-scan policy) decides whether to promote to prod. Either way the new version doesn't reach CI until someone has seen the diff. CodeCov's one-line exfiltration insertion would have been obvious in a diff review.

What this composes to. Pin once, fetch by hash, promote deliberately. The two-month CodeCov exposure window shrinks to whatever your promotion cadence is — typically a week for non-critical paths, immediate for paths your security team flags. The vendor-side compromise stops being your problem the moment your CI is reading from your own registry.

Config sketch

Generic mirror with SHA pin.

Works the same way for any vendor-supplied install script, installer binary, or signed tarball. The general principle: treat third-party scripts the same way you'd treat third-party packages.

# Mirror a vendor install script as a content-addressable artifact.

orbital repo create vendor-installers \
  --format=generic --kind=remote \
  --upstream=https://codecov.io/bash \
  --refresh=on-promotion-only

# In CI, fetch by hash:
orbital fetch vendor-installers/codecov-uploader@sha256:abc…

For your records

CodeCov timeline.

2021-01-31
Attacker uses credentials extracted from a flawed CodeCov Docker image build process to modify the bash-uploader served at codecov.io/bash. The modification adds a single line that POSTs the CI runner's environment to attacker infrastructure.
2021-04-01
A CodeCov customer verifying the bash-uploader's SHA-256 against the published documentation page notices a mismatch. They report it to CodeCov within hours.
2021-04-15
CodeCov publicly discloses the compromise. Affected window 2021-01-31 → 2021-04-01. Customers begin secret-rotation triage. Twilio, HashiCorp, Confluent, Rapid7, Mercari, and others confirm impact and rotate credentials over the following weeks.

Honest caveats

What this does not claim.

Pinning shifts the trust window, doesn't eliminate it. If you promote a poisoned version through your own review process — because the malicious change is hidden inside a 600- line shell script no reviewer reads carefully — the pin doesn't save you. Pinning catches vendor-side surprises; it doesn't replace a competent review.

It's friction for legitimate updates. Refresh-on-promotion means your CI lags slightly behind the vendor's latest version. For most CI tooling that's fine — the upgrade is non-urgent. For tooling where the latest version really matters fast (e.g. CVE-fix releases), you'll want a faster promotion cadence or a "promote-on-passing-scan" automation.

Generic-format is not as expressive as Maven or npm. It's a content-addressable blob store, no resolver, no dependency graph. Fine for shell scripts and standalone binaries; the wrong primitive for ecosystem packages where you'd want OrbitalReg's full format-specific gates instead.

Primary sources

Want a single pattern for every vendor-supplied script?

Talk to us about SHA-pinned CI fetches.

Rico, the founder, walks you through which third-party install scripts in your CI are worth pinning today, and how to fit the promotion cadence to teams who can't accept a one-week vendor lag.