Supply-chain case study · retrospective
CodeCov bash-uploader · April 2021
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
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. 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
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
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.
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."
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.
Config sketch
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.io/bash. The modification adds
a single line that POSTs the CI runner's environment to
attacker infrastructure.
Honest caveats
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?
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.