Incident Analysis

tj-actions Compromise: One Year Retrospective

A year after the tj-actions/changed-files compromise leaked CI secrets across thousands of GitHub repos, what did we fix and what is still dangerously convenient?

Shadab Khan
Security Engineer
8 min read

The tj-actions/changed-files compromise of March 2025, cataloged as CVE-2025-30066, was the incident that finally forced a lot of security teams to treat GitHub Actions references as supply chain artifacts rather than magic YAML strings. The tj-actions/changed-files action was used by more than 23,000 public repositories, and on March 14, 2025 an attacker pushed a malicious update that retroactively rewrote multiple tags to a commit that dumped CI runner memory, including secrets, into publicly accessible GitHub Actions logs. One year later this retrospective revisits what was confirmed, what was downstream, and how little some organizations have changed.

What Is the tj-actions Compromise?

The tj-actions compromise was a supply chain attack against tj-actions/changed-files, a popular GitHub Action that lists files changed in a pull request or push. On March 14, 2025, an unauthorized actor pushed a commit to the repository and rewrote multiple version tags (v1, v2, v35, v44.5.1, and others) so they all pointed to a single malicious commit, 0e58ed8671d6b60d0890c21b07f8835ace038e67. The payload executed a base64-decoded Python script that downloaded a secondary script and exfiltrated the contents of the runner's /proc/self/memory, including environment variables and secrets, into the publicly readable GitHub Actions workflow log of any repo running the action.

StepSecurity first flagged the anomaly. Within hours, GitHub removed the action, the maintainer rotated the repo, and Palo Alto Unit 42, Wiz, and CISA published advisories. Because many public repositories log their Actions output publicly, any secret that an affected workflow touched during the window of compromise was effectively disclosed to the internet.

What Is the Confirmed Timeline?

  • March 14, 2025, approximately 16:00 UTC: The malicious commit is pushed and the tags are force-updated.
  • March 14, 2025, within hours: StepSecurity detects and publishes initial IoCs.
  • March 15, 2025: GitHub removes the action from the Marketplace, the repository is quarantined, and the maintainer begins remediation.
  • March 15-17, 2025: Palo Alto Unit 42, Wiz, and Aqua independently analyze the payload. CISA adds CVE-2025-30066 to the Known Exploited Vulnerabilities catalog on March 18.
  • March 18-20, 2025: Second-order compromises surface. Reviewdog's reviewdog-action-setup and several other actions are found to have been compromised through the same attacker's earlier pivots, traced to a personal access token (PAT) of a maintainer whose credentials were harvested through a separate compromise of reviewdog/action-setup.
  • April 2025: Postmortems from Wiz and Aqua connect the dots: the attacker gained access via a compromised PAT on an unrelated action (reviewdog), used that access to plant a payload in reviewdog, and then leveraged a maintainer PAT exposed in a tj-actions workflow log to escalate into tj-actions itself.
  • Summer 2025: GitHub rolls out improved guardrails, including clearer documentation about pinning actions by commit SHA rather than by tag.

What Was the Root Cause, Publicly Reported?

The root cause chain combined two well-known weaknesses that had been discussed for years:

  • Version tags in Git are mutable by default. Unlike a container image digest, a tag like @v35 can be moved to point at any commit by the repo owner. Most GitHub Actions users reference actions by tag, not by commit SHA, which means they accept whatever the maintainer ships.
  • A personal access token belonging to the tj-actions maintainer had, at some point in its history, been captured in an earlier workflow run. The attacker traced that PAT back by mining public Actions logs from the reviewdog compromise. That gave them the write access needed to force-update tags.
  • The GitHub Actions runner environment exposes sensitive secrets in memory during workflow execution. Because workflow logs are public by default on public repositories, once the malicious action printed those memory regions to stdout, they were world-readable.
  • No default guardrail on GitHub Actions prevented a tag from being rewritten to an arbitrary commit, nor warned consumers that a tag had been force-updated.

The supply chain impact was magnified because tj-actions/changed-files was a high-fan-in dependency. Wiz's telemetry estimated that thousands of public repos with secrets in scope executed the malicious payload during the window, including some belonging to large open source foundations and enterprise open source teams.

What Are the Supply Chain Implications?

The tj-actions compromise crystallized three supply chain truths that the industry had been slow to internalize:

  • GitHub Actions references are code, not configuration. An @v1 in a workflow file is the equivalent of npm install without a lockfile. You are trusting the maintainer to never be compromised and never to be malicious.
  • Public CI logs are a gold mine. Attackers have been grepping them for tokens for years, and the reviewdog-to-tj-actions pivot proved that a token leaked in one repo can compromise a completely unrelated high-fan-in dependency.
  • Second-order supply chain compromises are now a thing. The attacker did not start at tj-actions; they worked their way there. Defenders need to think about the graph of trust between actions, not just the single action they are adding to a workflow.

It is hard to overstate how invasive this class of attack can get. A single malicious commit on a high-fan-in action can exfiltrate secrets from tens of thousands of CI pipelines in a matter of hours, and those secrets go into public logs where anyone can retrieve them.

What Should Defenders Do Now?

  • Pin every GitHub Action to a full commit SHA, not a tag. Use tools like Dependabot, Renovate, or GitHub's own pinning suggestion to keep pins current. A pinned SHA cannot be rewritten.
  • Use GitHub's allowlist feature to restrict which actions can run in your organization. Most organizations need a surprisingly small set.
  • Rotate any secret that could have been in scope during the tj-actions window, and enforce scope boundaries so CI secrets cannot touch production. Short-lived OIDC-issued credentials are a far better pattern than long-lived PATs.
  • Make public workflow logs opt-in for your org, or scrub environment variables aggressively from logs. Default-public logs for repositories is a footgun.
  • Audit personal access tokens across the maintainers of dependencies you care about. If your organization maintains popular actions, fine-grained PATs scoped to a single repo are the minimum, and FIDO-enforced 2FA is required.
  • Monitor for tag rewrites on the actions you depend on. GitHub exposes the events, and any @v1 that suddenly points at a new SHA should be treated as a signal.
  • Use StepSecurity, Chainguard's Actions mirroring, or similar hardening tools that detect egress anomalies from runners.

How Has the Ecosystem Changed in a Year?

Measurable improvements: GitHub has added clearer warnings about tag rewrites, more organizations have adopted SHA pinning, and commercial CI-hardening products have grown in adoption. The OpenSSF's actions-hardening guidance has been updated and referenced widely. GitHub has also expanded OIDC support for short-lived cloud credentials so that fewer long-lived AWS or GCP keys need to sit in CI secret stores.

Less-measurable improvements: most enterprise pipelines still reference actions by major-version tag, most engineers still cannot explain the difference between a tag and a SHA in Git, and personal access tokens continue to be created liberally and scope-broadly. A year later, a hypothetical "tj-actions 2" would still hit most organizations badly.

What Are the Broader Lessons for the Industry?

Three lessons. First, mutable references are a supply chain anti-pattern. Whether it is a Git tag, a Docker image tag, or an NPM dist-tag, anything that lets a maintainer silently change what you execute is a latent vulnerability. Second, CI is production. Secrets, cloud credentials, and deployment authority live in CI, and CI-side compromises produce real-world consequences that rival or exceed production-side compromises. Third, high-fan-in open source components deserve high-touch protection. The maintainers of actions used by tens of thousands of repos should receive the same identity hygiene, audit, and backup-maintainer treatment that internal production services at any large tech company get.

How Safeguard.sh Helps

Safeguard.sh treats your CI/CD configuration as the supply chain artifact it is. Reachability analysis maps every GitHub Action reference in your pipelines to the repositories, secrets, and cloud credentials it actually touches, filtering 60-80% of noise and highlighting mutable-reference usage that still points to a tag rather than a commit SHA. Griffin AI autonomously opens PRs to pin actions to verified SHAs, rotate exposed tokens, and swap compromised actions for safe forks within minutes of a CISA KEV addition. SBOM generation and ingest captures CI dependencies alongside application dependencies, giving you a single graph to query when the next tj-actions-class event hits. TPRM workflows score the upstream maintainers of your high-fan-in actions and surface the graph of trust between them, with 100-level dependency depth so second-order compromises (like reviewdog to tj-actions) are visible. Container self-healing rebuilds runner images with clean toolchains so a compromised action cannot persist across jobs.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.