Summary
tj-actions/changed-files was one of the most widely used GitHub Actions on the planet, wired into the CI pipelines of more than 23,000 repositories just to list which files a pull request touched. On 14 March 2025 it was turned into a weapon. An attacker who had stolen the credentials of the project's bot account rewrote nearly every version tag, from v1 all the way to v45.0.7, so they all pointed at one malicious commit. Any pipeline that pulled the action then ran code that dumped the build runner's memory and printed the secrets it found, AWS keys, GitHub tokens, npm tokens, private keys, straight into the workflow logs. Tracked as CVE-2025-30066, it is the defining modern CI/CD supply chain attack, and a blunt lesson that a version tag is a sticky note, not a lock.
How it happened
Workflows pull this action with a line like uses: tj-actions/changed-files@v45. The weakness is that a Git tag is mutable: it is just a pointer the repository owner can move to any commit at any time. The attacker, holding a Personal Access Token belonging to the project's @tj-actions-bot account, force-updated tags v1 through v45.0.7 so every one of them resolved to a single commit, 0e58ed8. To blend in, that commit was authored under a spoofed renovate[bot] identity (an unverified commit; the real Renovate bot was never touched). The payload was buried in the action's compiled dist/index.js bundle, not the human-readable source, as a new function that ran a double base64 encoded shell command. That command pulled a Python script (memdump.py) from a public gist, found the GitHub Actions Runner.Worker process, read its memory through /proc/<pid>/mem, and regex-matched the in-memory secrets table. The ugly twist: there was no attacker server. The harvested values were double base64 encoded and printed into the job's own console output, so the secrets were exfiltrated through the build logs themselves, which on a public repository anyone can read.
The cascade that fed it
The part worth studying is that the compromise did not start at tj-actions; it arrived through a chain of trusted dependencies. Palo Alto Unit 42 traced the origin to the spotbugs/spotbugs project, where a maintainer's Personal Access Token was added to a workflow in late November 2024 and stolen through a malicious pull request around 6 December 2024. That token had write access to reviewdog, another popular Actions publisher. On 11 March 2025 the attacker used it to repoint reviewdog/action-setup@v1 to a malicious commit (tracked separately as CVE-2025-30154), ran the same memory dumping trick, then quietly reverted the tag to hide the change. Because tj-actions/changed-files used a reviewdog action inside its own CI, with a token attached, that two hour window was long enough to capture the @tj-actions-bot PAT. Three days later that stolen token was used to poison tj-actions. One leaked secret in an obscure static analysis project cascaded, dependency by dependency, into 23,000 downstream pipelines. It is a supply-chain attack on the tools that build software, the same family as Codecov and SolarWinds.
How bad it really was
The headline, 23,000 affected repositories, is the count that used the action, not the count that lost secrets. The honest blast radius was much smaller. Endor Labs reviewed the actual runs and found that only about 614 repositories executed an affected workflow during the roughly 24 hour window, and only 218 leaked a secret into their logs. Most of what leaked was the ephemeral GITHUB_TOKEN, which expires when the job ends, so it was low value; only a few dozen runs exposed long lived credentials such as Docker Hub, npm, or AWS keys. Because the exfiltration channel was the logs, the real exposure sat on public repositories where anyone could read them, while private repositories were largely shielded. And the 23,000 were never the point. Unit 42 showed the campaign began as a precise strike on Coinbase's open source agentkit project, which Coinbase confirmed did no damage; the mass spray went out hours later the same day, opportunistic, after the targeted phase failed. No actor was ever identified, only throwaway accounts deleted afterward. CISA added the flaw to its Known Exploited Vulnerabilities catalog on 18 March 2025; a clean v46.0.1 had already shipped on 16 March.
Why tj-actions still matters
The root cause is mundane and everywhere: a Git tag is mutable. Referencing an action as @v45 trusts the maintainer, and anyone who steals their token, to never move that tag to hostile code. The fix the whole industry converged on is to pin every third party action to a full length commit SHA, which cannot be moved, so even a hijacked tag cannot redirect you. The rest of the lessons echo every CI/CD breach before it: build runners hold the keys to everything, so scope CI tokens to least privilege, prefer short lived OIDC credentials over long lived secrets, and watch runner egress so a poisoned action cannot phone home. tj-actions is the incident that finally made commit pinning a default rather than a nicety, and the OpenSSF published a maintainers' guide to hardening CI/CD pipelines in its wake. If you run GitHub Actions, this is the case that should change how your workflows are written.
How to fix it
- Treat every secret exposed to an affected workflow run (roughly 12 to 15 March 2025) as compromised, and rotate all CI tokens, cloud keys, and registry credentials immediately.
- Upgrade tj-actions/changed-files to v46.0.1 or later, and pin it to that release's full commit SHA.
- Search your "changed files" job logs for a double base64 encoded blob (decode with `base64 -d | base64 -d`) to confirm whether secrets actually leaked.
- Audit every other third party action in your workflows and re-pin them all to commit SHAs.
How to avoid it
- Pin every third party GitHub Action to a full length commit SHA, never a mutable tag or branch.
- Default GITHUB_TOKEN to read only, and grant write scopes per job only where strictly required.
- Replace long lived secrets with short lived OIDC credentials wherever the platform supports it.
- Monitor and restrict CI runner egress so a compromised action cannot dump secrets or reach an external server.
- Rotate any secret exposed to a compromised action fast, and keep a vetted, minimal set of actions instead of many community ones.
References
- https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
- https://www.wiz.io/blog/github-action-tj-actions-changed-files-supply-chain-attack-cve-2025-30066
- https://unit42.paloaltonetworks.com/github-actions-supply-chain-attack/
- https://www.endorlabs.com/learn/blast-radius-of-the-tj-actions-changed-files-supply-chain-attack
- https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction
- https://nvd.nist.gov/vuln/detail/CVE-2025-30066
Related vulnerabilities
All Supply chain →- CRITICALNPM-REDHAT-MIASMA-2026
In June 2026 the npm supply-chain worm finally produced something genuinely new and frightening: malicious packages that were signed and carried valid build provenance. Attackers used a compromised GitHub account inside Red Hat's own organisation to publish trojanised versions of 31 packages in the @redhat-cloud-services namespace through Red Hat's legitimate CI pipeline, so the poisoned releases arrived with authentic attestations proving they were built by the real build system. The payload, nicknamed Miasma, was a lightly re-skinned copy of Mini Shai-Hulud, a worm that a group called TeamPCP had open-sourced for anyone to reuse weeks earlier. No Red Hat customers were harmed, because Red Hat strips install scripts before deployment, but the permanent lesson is uncomfortable: a signature proves where a package was built, not that the build was clean.
- CRITICALNPM-AXIOS-2026
On 31 March 2026 the single most widely used HTTP client in the JavaScript world was turned into malware for about three hours. Axios carries well over 100 million weekly downloads and sits as a transitive dependency underneath a huge share of the npm ecosystem, so when a North Korea-nexus actor took over the maintainer's account and published two booby-trapped versions, the blast radius was enormous even though the bad versions were live only for one early-morning window. The poisoned releases pulled in a hidden dependency that ran on install and dropped a cross-platform remote access trojan onto developer machines and CI runners. It is the case study in why a few hours of a popular package being compromised is still a global incident, and in why a maintainer's own laptop is now a nation-state target.
- CRITICALNPM-SHAI-HULUD-2-2025
Shai-Hulud is the nightmare the npm ecosystem had long feared: a self-replicating worm. First seen in September 2025 and back in a more aggressive wave around 21-24 November 2025 ("The Second Coming"), it does not just poison one package and wait. When its malware runs in a developer's environment, it harvests every secret it can find, npm tokens, GitHub tokens, cloud keys, then uses those stolen npm tokens to automatically publish itself into other packages the victim maintains, spreading from maintainer to maintainer on its own. The second wave hit more than 25,000 GitHub repositories across roughly 500 compromised accounts, leaked the stolen secrets into public repos, and, if it failed to steal credentials, tried to wipe the victim's home directory. It is the moment supply-chain malware learned to propagate like a biological infection.
- CRITICALNPM-SHAI-HULUD-2025
Shai-Hulud, in September 2025, was the moment the npm ecosystem's oldest nightmare came true: a worm that spreads by itself. It began with a wave of compromised packages, the most prominent being @ctrl/tinycolor (over two million weekly downloads), and from there it did something no npm attack had done before. When its malware ran on a developer's machine, it hunted for every credential it could find, then used the developer's own npm token to republish itself into all of their other packages automatically, with no attacker involvement, jumping from maintainer to maintainer like an infection. More than 500 packages were compromised, including some from CrowdStrike. It is the first true npm worm (tracked as GHSA-6m4g-vm7c-f8w6 and GHSA-f3v2-259r-f6jc), the template for the more aggressive Second Coming that followed weeks later, and the ancestor of an entire 2026 lineage that ended with the worm's code being open-sourced for anyone to reuse.
- HIGHCVE-2026-62680
Orval: Generation-time SSRF + remote/local file inclusion via unrestricted $ref
- CRITICALGHSA-93qj-5q5v-3c2h
Trojanized pantheon-agents 0.6.1 and 0.6.2 on PyPI ship a credential stealer (supply-chain account compromise)