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

  1. 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;
  2. Least privilege. Put permissions: contents: read at the top of the workflow and add only what is needed; the narrower the default token, the cheaper the leak;
  3. 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).

← Back to all posts

Comments

…