On April 1, 2021, Codecov disclosed that an attacker had been quietly modifying its Bash Uploader script in Google Cloud Storage since January 31 — a two-month window during which every CI job that ran the uploader shipped its full environment variable set to a server the attacker controlled. The entry point wasn't a code vulnerability at all: it was a GCP HMAC key sitting in an intermediate layer of Codecov's self-hosted Docker image, a layer nobody thought to scan because it never appeared in the final image. Roughly 23,000 customers had AWS keys, database credentials, and API tokens exposed before a customer's SHA256 mismatch tipped off the company. Less than two years later, CircleCI told every customer to rotate every secret at its source — not just inside CircleCI — after an engineer's malware-infected laptop let an attacker steal a session token and exfiltrate environment variables straight from CircleCI's databases. Both incidents, and the GitHub Actions workflow-injection wave that followed in 2025, share the same root cause: environment variables are treated as ambient, disposable context, when in CI/CD they are frequently the highest-value target in the entire pipeline. This piece walks through how that leakage actually happens and what stops it.
What actually happened in the Codecov and CircleCI breaches?
Both breaches started outside the environment variables themselves and ended by harvesting them wholesale. In Codecov's case, the attacker pulled a GCP HMAC key out of an intermediate layer of Codecov's Docker image-build process — a layer that persisted in the image history even though it didn't appear in the final published image — and used that key to edit the live Bash Uploader script hosted in GCS. The added line sent the CI environment's full variable set, including tokens and credentials unrelated to Codecov, to a third-party server on every job that downloaded and ran the script between January 31 and April 1, 2021. CircleCI's incident, disclosed January 4, 2023, started with an engineer's laptop being compromised by malware capable of stealing session cookies; the attacker used a valid session with production access to exfiltrate customer secrets and environment variables directly from CircleCI's data stores between December 19 and December 22, 2022. Neither company was breached through a vulnerable dependency — both lost control of a system that had legitimate, broad access to other people's environment variables.
How do secrets end up baked into container image layers?
Docker images are layered, and a layer is immutable and cacheable even after a later RUN command deletes the file it added — the bytes remain in that layer's tarball, retrievable by anyone who can pull the image or inspect its history. A common pattern is ARG or ENV used to inject a credential mid-build (for a private package registry, for example), followed by a later step that "cleans up" by unsetting it — the value is still recoverable with docker history or by extracting the layer tarball directly, which is exactly the mechanism that exposed Codecov's GCP key. Multi-stage builds reduce this risk by discarding intermediate stages entirely rather than relying on rm inside a single stage, and BuildKit's RUN --mount=type=secret avoids writing the secret into any layer at all. Any pipeline that still relies on ARG for credentials, publishes intermediate build stages to a registry for caching, or reuses a base image without re-auditing its layer history is exposed to the same class of failure that cost Codecov roughly two months of undetected exfiltration.
How do GitHub Actions workflows leak secrets to untrusted code?
GitHub Actions has its own well-documented leakage pattern: workflows triggered by pull_request_target run with the base repository's full secret access while checking out and executing code from a fork, a combination widely called a "pwn request." CVE-2025-47928, disclosed in the spotipy project, is a concrete instance — the integration-tests workflow used pull_request_target, installed a forked PR's setup.py via pip, and let attacker-controlled code exfiltrate the GITHUB_TOKEN plus the SPOTIPY_CLIENT_ID/SPOTIPY_CLIENT_SECRET secrets, with a CVSS score of 9.1. The tj-actions/changed-files compromise in March 2025 (CVE-2025-30066) was broader still: an attacker compromised the tj-actions-bot PAT, rewrote every version tag of the action to point at malicious code, and that code downloaded a Python script that dumped GitHub Actions runner memory to expose secrets in build logs — affecting an action used in more than 23,000 repositories before GitHub and the maintainers patched it in v46.0.1. In both cases, the leak came from workflow permissions design, not from a flaw in the secret storage itself.
Why do secrets end up printed in build logs and telemetry?
Environment variables get forwarded far more often than teams realize, because CI runners pass the full environment to every child process by default. A printenv or env call inside a malicious or compromised dependency's install script — the same npm and PyPI supply-chain pattern used repeatedly since 2021 — captures every variable the job has, not just the ones the package legitimately needs. Verbose build tooling is an equally common source: a misconfigured set -x in a shell script, a debug-mode HTTP client that logs request headers, or an error-reporting SDK that attaches the process environment to crash reports can each put a database URL or API key in plaintext in a log that's retained for months and readable by anyone with log-viewer access. The tj-actions incident's memory-dump step relied on exactly this exposure surface — once secrets are in stdout, they're subject to whatever retention and access policy applies to logs, which is almost always looser than the policy on the secret store itself.
How Safeguard Helps
Safeguard's secrets scanning is built directly around these failure modes rather than treating them as edge cases. It scans build logs with redaction at the source, extracts and scans every container image layer (not just the final image), and runs deep Git history scans — the exact surfaces that let the Codecov HMAC key and CircleCI-style leaked tokens go undetected. Verified findings are checked against the issuing service itself (an AWS key against sts:GetCallerIdentity, a GitHub PAT against /user) so a security team isn't triaging theoretical matches; critical, verified findings trigger the revoke-rotate-purge playbook, calling the issuer's revoke API, opening a rotation PR, and optionally rewriting Git history with git-filter-repo. A pre-push git hook and IDE extension apply the same pattern set before a secret ever reaches a shared branch or a CI log, closing the gap that let a two-month-old tampered script go unnoticed at Codecov.