Codecov Bash Uploader - Credential Leaked by a Docker Build → One Line Added to a Script Everyone Curls → Two Months of CI Environment Variables Exfiltrated
On January 31, 2021 an attacker exploited an error in Codecov's Docker image creation process to extract a Google Cloud Storage key, and used it to modify the Bash Uploader - the script that tens of thousands of CI pipelines download and execute on every build to report coverage. The change was a single added line that exported the CI environment to a server the attacker controlled. It ran undetected for two months. What ended it was not monitoring: on April 1 a customer verifying the script's integrity noticed the SHA-256 of the file they had downloaded did not match the value published on GitHub. Codecov disclosed on April 15. The exposure covers every secret present in an affected pipeline, and the downstream list is the point - HashiCorp had to rotate a GPG signing key used for release verification, and Twilio, Rapid7 and Confluent were all affected. This is the earliest chain on this page in which the software supply chain is a build-time script rather than a package, and it is the direct ancestor of the LiteLLM / Trivy compromise five years later.
Container images are layered filesystems with history, and that history is the trap. A secret used during a build and deleted in a later layer is still recoverable from the earlier one, and anyone who can pull the image can pull the layers. Codecov's image creation process contained an error of this shape, and it exposed a GCS key with write access to the storage bucket serving the Bash Uploader script. Note the asymmetry that makes this so efficient: one key, no exploitation of the product itself, and write access to an artifact that tens of thousands of organizations fetch and execute automatically on every single build.
Extracted: A Google Cloud Storage key with write access to the uploader's bucket
Why images leak: Layers preserve history; deleting a secret in a later layer does not remove it
Value obtained: Write access to a script executed automatically by tens of thousands of pipelines
Date: January 31, 2021
The payload's restraint is what let it live for two months. No obfuscation, no second stage, no binary - one added line of bash that shipped the environment out. In a script that already collects coverage data and posts it over the network, an extra outbound call is close to invisible, and there is no runtime behaviour that would look wrong to anyone watching. The modification was applied periodically rather than once, which suggests the attacker was managing exposure deliberately and re-applying after Codecov's own deployments overwrote the file. This is the fundamental hazard of the curl | bash pattern: the artifact you execute is fetched fresh every time, so its integrity is a property of the hosting infrastructure on the day you run it rather than something you have ever verified.
Cadence: Applied periodically, likely re-applied after legitimate deployments
Why invisible: The script already posts data over the network as its normal function
Structural hazard:
curl | bash means integrity depends on the host, every run, unverified
The uploader runs late in a build, by which point the job holds everything it needed to compile, test, sign and deploy. Exporting the environment therefore captures the pipeline's entire secret inventory in one action, and it does so on every build across every affected repository for two months. HashiCorp's disclosure is the one worth dwelling on: the exposed material included the GPG private key used to sign and verify HashiCorp product releases, which is to say a supply-chain compromise of a CI tool reached the signing key underpinning another vendor's supply-chain integrity. Twilio, Rapid7 and Confluent also disclosed impact, and Rapid7 confirmed source code exposure.
Frequency: Every build, every affected repository, for two months
HashiCorp: Rotated the GPG private key used to sign and verify product releases
Also affected: Twilio, Rapid7 (source code exposure), Confluent
The recursion: A CI-tool compromise reached another vendor's release-signing key
Two months of continuous exfiltration ended because one customer did the integrity check that the documentation offered and almost nobody performed. That is worth stating plainly rather than framing as a happy ending: the control existed, it worked perfectly, and it was optional, so it caught the attack sixty days late. Codecov investigated, disclosed publicly on April 15, and by April 29 was telling affected organizations which specific environment variables had been exposed. There is a general principle here that recurs across this page - an integrity check that is documented but not enforced is a control you do not have. Compare the release-tag verification that would have caught LiteLLM, or the hash pinning that would have caught npm debug/chalk: available, cheap, and skipped.
April 15, 2021: Codecov discloses publicly
April 29, 2021: Affected organizations told which environment variables were exposed
Detection source: One customer performing an optional integrity check
General principle: An integrity check that is documented but unenforced is not a control
Codecov reported roughly 29,000 customers at the time, with reporting placing over 23,000 as potentially affected during the window. But the meaningful number is not customers - it is the systems reachable with the credentials those customers' pipelines happened to hold, which is unknowable from the outside and was, for many organizations, unknowable from the inside too. That is the recurring difficulty with development-infrastructure compromise: the affected party has to enumerate every secret that has ever passed through CI, and most organizations discover during the incident that no such inventory exists. Response therefore means rotating everything, and rotating everything is expensive precisely because nobody planned for it.
Real blast radius: Whatever those pipelines' credentials could reach - unknowable without an inventory
Required response: Rotate every secret that passed through CI during the window
Why that is hard: Most organizations have no inventory of CI secrets to rotate against
🛡 How to Defend Against This Chain
curl | bash in a pipeline - pin the artifact and verify it every run. This is the control that ends the chain. Vendor the uploader or installer at a known version, verify its checksum or signature before executing, and fail the build on mismatch. The customer who found this proved the check works; the two months proved that documenting it is not the same as enforcing it. Any third-party script fetched at build time should be treated exactly like a dependency, because that is what it is.--mount=type=secret), never COPY credential files even if a later layer deletes them, and run a scanner over published images in CI to catch it when someone does. Assume anything that ever existed in a layer is public.