A supply-chain reconstruction. In March 2025 something like 23,000 repositories pulled a compromised version of a GitHub Action for a couple of days, and the credential theft that followed is the boring part of the story. The interesting part is what the whole ecosystem trusted to get there. A name, not a hash.
The chain
2 CVEs, one cascade. The first link was CVE-2025-30154, a compromise of reviewdog/action-setup, which is believed to have leaked a Personal Access Token belonging to the automation account behind tj-actions. With that token the attacker did not need an exploit. They retagged tj-actions/changed-files: the tags v1 through v45.0.7 were all moved to point at a single malicious commit, 0e58ed8. That is CVE-2025-30066. One stolen token in one link of the chain, and every downstream repo that referenced the action inherited the compromise.
What the payload did
The malicious commit added a base64-encoded Node function. It pulled down a Python script that walked the memory of the GitHub Runner process, Runner.worker, looking for secrets by regular expression, and wrote what it found into the build log. On a public repository the build log is public. Access keys, GitHub PATs, npm tokens, private RSA keys; printed to a page anyone could read. There was no memory-corruption bug and no clever primitive. The CI system was asked for its secrets and it published them.
The part worth the writeup
Every one of those repositories referenced the action the ordinary way: uses: tj-actions/changed-files@v45. A tag. Git tags are mutable; a tag is a label someone can move, and whoever controls the label controls what runs inside your pipeline. The maintainer’s stolen token controlled the label. So the reference looked pinned and was nothing of the sort; it was a name pointing at whatever the name’s owner decided that morning.
That is the gap the Actions ecosystem left open, and it is not specific to tj-actions. The convention is to trust references, not content. Pin the same action to its full commit SHA instead of a tag and the retag does nothing, because the SHA is the content and the attacker cannot move content without changing it. Almost nobody pins. The whole registry runs on names that their owners, or whoever steals an owner’s token, can repoint at will.
There is a reason almost nobody pins, and it is the reason most of these things happen: the convenient path and the safe path point in opposite directions. A tag reads cleanly and tracks updates on its own; a 40-character commit hash is opaque and somebody has to bump it by hand. The tooling supports pinning; the defaults never pushed it; an entire ecosystem took the readable option and inherited the trust assumption that came with it. That is how one moved tag reached 23,000 repositories at once. The attacker did not beat the trust model. They used the one everybody had already opted into.
Patch is not rotation
The tags were reverted and tj-actions shipped a clean v46.0.1 inside a day. That closes the door. It does not rotate a single leaked key. Every secret that reached a public log during the window is still valid until a human rotates it by hand, and most of those humans learned about it from a writeup rather than from anything they were watching. This is the same shape as the ToolShell reconstruction from earlier this month; the fix removes the entrance, not the credential the attacker already carried out through it.
The window nobody was watching
The secrets sat in public build logs for a window measured in hours before the wider disclosure caught up. The signal was sitting right there the whole time: an action resolving to a commit SHA nobody recognized, a runner process reading its own memory, credential-shaped strings landing in build output. Catching any of that means watching CI and build telemetry in close to real time, which almost nobody does, because the pipeline is the one piece of infrastructure teams still treat as trusted rather than as attack surface. It is exactly the kind of watching that teams without a 24-hour function hand to a managed detection provider; someone whose job is the window between the push and the disclosure, which is precisely where this compromise lived. The Wiz writeup and CISA’s advisory both walk the indicators; the point is that they are indicators a defender could have seen live, not just in the postmortem.
Three moves, none of them new
Steal a token from one link in the chain; use it to move a tag that thousands of others trust; read the secrets the pipeline prints for you. The remediation is equally old: pin to hashes and not names, rotate everything that could have leaked in the window, and treat the build system as something that can be attacked, because it just was. The supply chain trusted a name. It usually does.