A retargeted tag: twenty thousand repos leaked secrets into build logs
In March 2025 the version tags of tj-actions/changed-files were pointed at a malicious commit, and CI tokens were printed into workflow logs. CVE-2025-30066 is the bill for mutable tags.
In March 2025, CI logs across many repositories suddenly contained output that did not belong there: base64-encoded environment variables. They came from an action almost everyone had used — tj-actions/changed-files.
The CVE is CVE-2025-30066; the companion issue in reviewdog/action-setup@v1 is CVE-2025-30154. CISA published a supply-chain alert on March 18.
What happened
The attacker did not break into GitHub, and did not poison npm. They changed which commit the tags pointed to:
- several version tags of tj-actions/changed-files were retargeted at a malicious commit;
- the malicious code printed CI tokens and secrets into the workflow log while the action ran;
- because logs are visible to repository collaborators by default, some of them leaked from public repositories.
Any workflow containing uses: tj-actions/changed-files@v45 would pull the rewritten commit on its next run. Post-incident counts put the number of affected repositories above twenty thousand.
Why a tag can kill you
In uses: owner/repo@ref, the ref can be a branch, a tag, or a full commit SHA. The first two are mutable:
- branches move forward;
- tags can be deleted and re-pointed at another commit.
That is normal Git behaviour. But in a world where “someone else’s code runs inside your CI with your permissions”, a mutable ref hands execution rights to any future push upstream. And CI permissions are rarely small: repository write access, package publishing, cloud credentials.
Three lines of defence
- Pin to a commit SHA.
uses: tj-actions/changed-files@<40-char SHA>, and let Dependabot or Renovate bump the SHA. It is the only thing that survives a tag rewrite; - Least privilege. Put
permissions: contents: readat the top of the workflow and add only what is needed; the narrower the default token, the cheaper the leak; - Assume logs are public. Do not put secrets into environment variables for third-party scripts to read; use OIDC to mint short-lived credentials instead of long-lived tokens.
Two more things are worth doing now: audit workflow runs since March for odd output, and rotate every token that could have been exposed — npm, Docker Hub, cloud providers included.
The judgement
Supply-chain attacks are usually misread as “the package I use is trustworthy”. The real situation is: a trustworthy package can be re-pointed one day, and your CI will pull and execute it unattended at 3 a.m. Pinning refs to SHAs is the cheapest, most certain step available.
Cover image: tj-actions/changed-files repository (GitHub).

Comments
…