Cloud Security Office Hours Banner

Codecov Bash Uploader 2021

Step-by-step kill chain mapped to MITRE ATT&CK Cloud, sourced from official post-mortems and primary technical analyses.

January-April 2021 Critical CI/CD Tooling

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.

2 months undetected: January 31 to April 1, 2021
One line of added bash was the entire payload
Found by a customer checking a hash, not by monitoring
Downstream: HashiCorp GPG signing key, Twilio, Rapid7, Confluent
📄 Codecov - Bash Uploader security update ↗ 📄 GitGuardian - Codecov supply chain breach breakdown ↗
Initial Access - A Credential Left in a Container Image
01
An error in Codecov's Docker image creation process let an attacker extract the Google Cloud Storage key that controlled the hosted Bash Uploader
T1552.001 - Credentials In Files T1078.004 - Valid Accounts: Cloud Accounts

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.

Root cause: An error in the Docker image creation process
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
Docker Layer LeakGCS KeyArtifact Write AccessT1552.001
📝 Supply Chain - Modify the Script, Repeatedly
02
The attacker periodically altered the hosted Bash Uploader to add one line exporting the CI environment to their own server
T1195.002 - Compromise Software Supply Chain

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.

Payload: A single added line exporting CI environment variables to an attacker server
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
curl | bashOne-Line PayloadRepeated ModificationT1195.002
🔑 Credential Access - Everything the Pipeline Was Holding
03
Every environment variable in the CI job left with each build: cloud keys, registry tokens, signing keys, database credentials, API tokens
T1552.007 - Unsecured Credentials: Container API T1041 - Exfiltration Over C2 Channel

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.

Captured: The full CI environment - cloud keys, registry and repo tokens, signing keys, database credentials
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
CI SecretsGPG Signing KeySecond-Order ImpactT1552.007
🔍 Detection - A Customer Compared Two Hashes
04
On April 1, 2021 a customer verifying the uploader's integrity found the SHA-256 of the downloaded script did not match the value published on GitHub

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 1, 2021: A customer's SHA-256 check fails against the published value
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
Hash MismatchCustomer DetectionOptional Control
🌊 Impact - The Blast Radius Is Everyone Else's Pipelines
05
Codecov's own systems were barely the point; the damage was distributed across thousands of customers' build environments and the systems those credentials reached
T1199 - Trusted Relationship

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.

Customer base: ~29,000 at the time; 23,000+ reported as potentially affected in the window
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
Distributed Blast RadiusMass RotationDev InfrastructureT1199

🛡 How to Defend Against This Chain

Never 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.
Stop putting long-lived secrets in the CI environment at all. The payload's whole value was that everything sat in environment variables waiting to be read. Replace static cloud keys with OIDC federation so credentials are minted per job and expire in minutes, scope every remaining secret to the single job that needs it, and split jobs so no step holds both third-party tooling and the credentials it could steal. That last point is exactly what the LiteLLM compromise repeated five years later.
Keep secrets out of container images, and prove it. The initial access was a build-time credential recoverable from image layers. Use build secrets that do not persist into layers (BuildKit --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.
Constrain egress from build runners so a one-line exfiltration fails. The payload sent data to an arbitrary host from inside the pipeline. Default-deny outbound from runners with an allow-list of the registries, artifact stores and vendor endpoints you actually use, and alert on connections to anything else. This single control breaks the exfiltration step in Codecov, Megalodon, and LiteLLM alike.
Maintain a CI secret inventory before you need it. The expensive part of this incident for most victims was working out what to rotate. Keep a current list of every secret available to each pipeline, its owner, its blast radius, and the command that rotates it - then rehearse a full rotation. An organization that can rotate everything in an afternoon treats this class of incident very differently from one that cannot enumerate its own secrets.
Monitor your published artifacts from the outside. Codecov did not detect that its own hosted script had been altered for two months. If you publish something customers download and execute, continuously verify from an external vantage point that what is being served still matches what you intended to serve - the same argument this site applies to its own SRI and edge-header checks.

Related defense topics