Cloud Security Office Hours Banner

tj-actions/changed-files 2025

Step-by-step kill chain mapped to MITRE ATT&CK Cloud, sourced from official post-mortems and primary technical analyses.

March 2025 Critical GitHub Actions

tj-actions/changed-files - Stolen PAT → Retag to Malicious Commit → Runner Memory Dump → Secrets in Public Logs → 23,000+ Repos Exposed

In March 2025 attackers ran a chained supply chain compromise (CVE-2025-30066) against tj-actions/changed-files, one of the most widely used GitHub Actions. Using a stolen bot Personal Access Token, they retroactively updated many version tags to point at a single malicious commit that scraped the CI runner's memory and printed captured secrets - AWS keys, GitHub PATs, npm tokens, and private RSA keys - into publicly readable GitHub Actions build logs. Over 23,000 repositories referenced the action, though only dozens are confirmed to have leaked live credentials. The initial foothold traced back to a compromised PAT tied to reviewdog/action-setup (CVE-2025-30154), making this a supply-chain attack on a supply-chain tool.

CVE-2025-30066 tj-actions/changed-files
23,000+ repositories referenced the action
Mar 12-15 2025 exposure window (UTC)
Root cause: stolen bot PAT (reviewdog chain, CVE-2025-30154)
📄 Wiz Threat Research analysis ↗ 📄 CISA advisory ↗
🕵️ Reconnaissance - Target a Ubiquitous Action
01
Attackers selected tj-actions/changed-files, an action referenced by tens of thousands of public CI pipelines
T1593 - Search Open Websites and Domains

tj-actions/changed-files detects which files changed in a pull request or commit and is one of the most popular GitHub Actions, referenced by more than 23,000 repositories. Because so many public repositories invoked it - often pinned only to a mutable version tag like v45 rather than a commit SHA - a single poisoned release would fan out into thousands of CI/CD runs. Attackers who could push to the action would inherit the trust and the secrets of every downstream workflow that ran it.

Target: tj-actions/changed-files (file-change detection action)
Blast radius: 23,000+ repositories referencing the action
Why it worked: most callers pinned to mutable tags (v45, v44...) not immutable commit SHAs
Supply ChainGitHub ActionsMutable TagsT1593
Initial Access - Chained Compromise via a Stolen Bot PAT
02
A compromised bot Personal Access Token - traced to the reviewdog/action-setup chain - gave write access to the tj-actions repository
T1078 - Valid Accounts

The tj-actions maintainers disclosed that the root cause was a compromised GitHub Personal Access Token (PAT) belonging to the tj-actions-bot account. That PAT was exposed earlier through reviewdog/action-setup@v1 (CVE-2025-30154), which had itself been poisoned to dump secrets. tj-actions depended on reviewdog transitively via tj-actions/eslint-changed-files, so credentials leaked during the reviewdog attack were reused to gain write access to changed-files. Analysts including Unit 42 traced the initial foothold further upstream through SpotBugs' GitHub Actions workflow, making this a multi-hop chained supply-chain compromise.

Stolen credential: tj-actions-bot GitHub PAT (write access to the action repo)
Upstream chain: SpotBugs workflow → reviewdog/action-setup (CVE-2025-30154) → tj-actions-bot PAT
Dependency link: tj-actions/eslint-changed-files relied on reviewdog/action-setup
Compromised PATreviewdogCVE-2025-30154T1078
🔍 Execution - Retag Versions to a Malicious Commit
03
Attackers pushed a malicious commit and retroactively repointed many existing version tags to it
T1195.001 - Compromise Software Dependencies and Tools

With write access, the attackers committed a malicious payload and then rewrote many of the action's existing version tags so that they all resolved to that single commit. Consumers who trusted a tag such as v45 - assuming it was an immutable, reviewed release - silently began running attacker code on their next CI run. The injected code was a Node.js function containing base64-encoded instructions that downloaded a Python script from a GitHub gist. Because the tags were mutable, no consumer had to change a line of their workflow to be affected.

Technique: retroactively updated many version tags to one malicious commit
Dropper: Node.js function with base64-encoded instructions pulling a Python script from a GitHub gist
Stealth: mutable tags meant existing workflows executed the payload with no config change
Tag RetaggingMalicious CommitCVE-2025-30066T1195.001
📤 Collection - Dump Runner Memory, Extract Secrets
04
The Python payload scraped the GitHub runner's memory for credentials and wrote them into the build log
T1552.001 - Credentials In Files

The downloaded Python script scanned the memory of the GitHub Actions runner process, where workflow secrets are held in cleartext during a job, and harvested any credentials it found. Captured material included valid AWS access keys, GitHub Personal Access Tokens, npm tokens, and private RSA keys (T1552.004). Rather than exfiltrate to an external server, the payload printed the stolen secrets directly into the job's standard output as a double-encoded base64 blob - a low-noise technique that needed no outbound C2 channel and blended into normal log volume.

Method: Python memory scraper reading the runner process for in-memory secrets
Harvested: AWS access keys · GitHub PATs · npm tokens · private RSA keys (T1552.004)
Encoding: secrets emitted to the build log as a double base64-encoded payload
Memory ScrapingUnsecured CredentialsPrivate KeysT1552.001
💰 Impact - Secrets Exposed in Public Build Logs
05
On public repositories the captured secrets became readable by anyone with access to the workflow logs
T1213 - Data from Information Repositories

For public repositories, GitHub Actions logs are world-readable, so any secret written to the log by the payload was exposed to the entire internet during the March 12-15, 2025 window. More than 23,000 repositories referenced the action, but Wiz Threat Research confirmed live secret leakage in only dozens of cases - the rest either did not run the affected job with real secrets, used private repos, or caught it before rotation. The malicious gist and commit were taken down around March 15, and the maintainers shipped a clean release, v46.0.1. Any public workflow that ran between roughly 2025-03-12 00:00 and 2025-03-15 12:00 UTC should treat all its CI secrets as compromised and rotate them.

Exposure: public GitHub Actions logs are world-readable - leaked secrets fully public
Confirmed impact: 23,000+ repos referenced it; dozens confirmed to leak live secrets (Wiz)
Remediation: gist/commit removed ~Mar 15; fixed in v46.0.1; rotate all secrets from the exposure window
Secret ExposurePublic LogsCredential RotationT1213

🛡 How to Defend Against This Chain

Pin third-party Actions to a full commit SHA, not a mutable tag. A tag like v45 can be silently repointed by anyone who compromises the action repository, which is exactly what happened here. Pinning to a 40-character commit SHA makes a release immutable: attacker retagging cannot change what your workflow runs. Combine with Dependabot to propose reviewed SHA bumps for your supply chain.
Minimize and scope the secrets exposed to CI jobs. A memory-scraping payload can only steal what the runner holds. Grant workflows the least privilege possible, use short-lived OIDC federation to cloud providers instead of long-lived AWS keys, and avoid injecting broad PATs. Fewer standing secrets in the runner means a smaller blast radius when a step is malicious.
Restrict which Actions are allowed to run. GitHub lets you allowlist specific verified or SHA-pinned Actions at the org or repo level and block everything else. This stops an unvetted or newly poisoned action from executing in your CI/CD pipeline in the first place, and turns a supply-chain incident into a policy denial rather than a breach.
Monitor build logs and egress for anomalous behavior. The payload printed base64 secret blobs and pulled code from an external gist. Alert on unexpected outbound network calls from runners, on large or high-entropy base64 output in logs, and scan Actions logs with secret-scanning tooling so exfiltrated credentials are caught even when they never leave in a classic exfiltration channel.
Have a fast, rehearsed secret-rotation plan. When a dependency is compromised, speed of rotation decides the impact. Inventory every credential reachable from CI, know which workflows ran in the exposure window, and be able to revoke and reissue AWS keys, PATs, npm tokens, and signing keys within hours. Treat any secret that touched an affected runner as burned.

Related defense topics