On March 14, 2025, security teams woke up to CVE-2025-30066: the GitHub Action tj-actions/changed-files, embedded in more than 23,000 repositories, had been silently rewritten to dump CI runner memory — secrets included — into public build logs. The attacker didn't exploit a vulnerability in anyone's application code. They compromised a maintainer's personal access token, rewrote every version tag the Action had ever published, and waited for CI pipelines to pull "latest" automatically, exactly as designed. CISA issued an alert within days. It's the same structural failure that let attackers plant a trojanized installer on Piriform's CCleaner build servers back in 2017, reaching 2.27 million downloads before Cisco Talos caught it, and the same failure that let a hijacked npm account slip a cryptominer into ua-parser-js in October 2021, a library pulled in 8 million times a week by companies including Facebook, Amazon, and Microsoft. In each case, the compromised component wasn't a random dependency — it was a tool teams trusted specifically because it touched their build pipeline, their CI secrets, or their security posture. This piece looks at what happened in each case and what actually would have stopped it.
What happened when CCleaner's own build server was hijacked?
Attackers compromised Piriform's build infrastructure sometime before Avast's 2017 acquisition of the company closed, inserting a backdoor directly into the official, digitally signed CCleaner 5.33 installer. Because the malicious code shipped inside Piriform's own signed binary, neither users nor endpoint security products had reason to flag it — it was the legitimate update, distributed through the legitimate channel. Cisco Talos discovered the compromise on September 13, 2017, by which point 2.27 million users had downloaded the trojanized version. A second-stage payload was then selectively pushed to roughly 40 machines inside specific targets, including Google, Microsoft, Intel, Samsung, and Cisco, suggesting the mass infection was cover for a narrower espionage operation. The lesson wasn't "don't trust CCleaner" — it was that a build server is itself an attack surface, and a signature only proves the binary came from that pipeline, not that the pipeline was clean.
How did a stolen token turn a popular GitHub Action into a credential-leaking attack?
tj-actions/changed-files worked exactly as advertised for years before an attacker obtained a maintainer's personal access token and used it to force-push malicious commits, then rewrite every existing version tag — including pinned-looking ones like v35 — to point at the compromised code (CVE-2025-30066, GHSA-mrrh-fwg8-r2c3). The payload ran inside consuming repositories' CI jobs and dumped runner process memory into the workflow's own build log using a public gist, exposing any secrets present in that memory to anyone who could view the log — including, in public repositories, anyone on the internet. A related npm-based action, reviewdog/action-setup, was compromised through the same technique days earlier (CVE-2025-30154). GitHub and CISA both published advisories; Wiz and Semgrep independently documented the blast radius. Because most workflows referenced the Action by a mutable tag (@v35) rather than a commit SHA, every consumer automatically inherited the attacker's rewritten code on their next run — no update, review, or click required.
Why did a trusted, widely-used npm package become a cryptominer?
In October 2021, an attacker gained control of the maintainer account for ua-parser-js and published three malicious versions — 0.7.29, 0.8.0, and 1.0.0 — laced with a cryptominer and an infostealer targeting Linux and Windows machines that ran npm install. The package itself is a small, unglamorous browser-detection utility, but its reach was enormous: roughly 8 million downloads a week, embedded transitively in projects at Facebook, Amazon, Microsoft, and thousands of smaller teams, according to reporting from Aqua Security and Sonatype and incident analysis from Mandiant. Nothing about the package's popularity or age made it safer — an account takeover requires only one set of stolen maintainer credentials, and CI systems that auto-install on every build gave the payload a direct line into engineers' laptops and build servers within hours of publication.
What does least-privilege CI access actually look like for third-party tools?
Every incident above traces back to a third-party tool holding more CI trust than it needed: broad write access to secrets, unrestricted network egress, or execution with the same permissions as the code it was scanning. Concretely, that means scoping the token any scanner or Action receives to the narrowest GitHub permissions block it actually requires — contents: read instead of the default broad token — rather than accepting whatever access GitHub grants a workflow by default. It also means pinning every third-party Action to a full-length immutable commit SHA (uses: org/action@a1b2c3...) instead of a mutable tag or branch name, since tags are exactly what the tj-actions attacker rewrote; GitHub's own security hardening guidance recommends SHA-pinning for this reason. Treat a security scanner's CI credentials the same way you'd treat a production database credential — because functionally, in a compromised pipeline, that's exactly what it becomes.
How should you sandbox and verify a security tool before granting it CI access?
Run unfamiliar or newly-added scanners in an isolated job with no outbound network access and no secrets in scope on their first several runs, so a compromised tool has nothing to exfiltrate even if it tries. Where a vendor publishes checksums or signed releases, verify them before the binary ever executes inside CI rather than trusting curl | sh from a build script. And because none of these three incidents were caught by automated detection — Talos, GitHub, and independent researchers all found them through manual investigation and tips — instrument the pipeline itself: log and alert on unexpected outbound connections from build jobs, and review what a third-party Action or scanner actually reads and writes on each run, not just once at onboarding.
How Safeguard Helps
Safeguard applies the same scrutiny to the tools in your pipeline that it applies to your application dependencies. Every build generates a CycloneDX SBOM that includes your CI-time tooling and GitHub Actions, so a compromise like tj-actions/changed-files shows up as a queryable inventory hit rather than something you discover from a CISA alert. Guard's proxy deployment lets you front third-party MCP-based tooling with policy enforcement and an append-only audit trail of every call a tool makes — including denied and monitored requests — so a scanner or agent that starts behaving outside its expected pattern is visible in the audit log rather than invisible until secrets turn up in a public gist. Combined with reachability-aware SCA findings and AI-generated fix PRs, teams get one place to both vet what's running in CI and see what it actually did once it got there.