In March 2025, an attacker compromised the reviewdog/action-setup GitHub Action by exploiting an automated team-invitation flow to gain write access to the repository. They pushed a malicious commit containing a Python script that dumped runner memory - including secrets - to workflow logs. Then they used those stolen credentials to overwrite all 44 version tags of tj-actions/changed-files, a dependency used by over 23,000 downstream workflows. The tag v1 no longer pointed to the audited commit. It pointed to the attack payload.
The compromise ran undetected for four months.
Why Mutable Tags Are the Root Cause
The entire attack depended on one assumption held by the vast majority of GitHub Actions users: that a version tag is stable. It is not. A git tag -f command can silently redirect any tag to any commit. The maintainer does not need to push a new release. They just need write access to the repository - or someone who can obtain it does.
The tj-actions/changed-files repository had no tag protection rules. The reviewdog credential that enabled the attack came from a workflow that had secrets: inherit and no permission scoping. Both are default behaviour in many CI configurations.
The Datadog DevSecOps 2026 Report puts numbers to this: only 4% of organisations pin all marketplace actions to commit SHAs, 71% never pin any actions, and 80% use unpinned third-party actions not maintained by GitHub.
What Pinning Actually Means
The fix is to reference actions by their full commit SHA, not a mutable tag:
# Vulnerable - tag can be redirected
- uses: tj-actions/changed-files@v44
# Safe - SHA cannot be rewritten
- uses: tj-actions/changed-files@d6babd6899969df1a11d14c368283ea4436bca78
The SHA is immutable. An attacker who rewrites the tag cannot change what d6babd6b... resolves to. Runners fetch by SHA if it is specified, bypassing the tag entirely.
GitHub’s August 2025 changelog added native enterprise policy enforcement for SHA pinning. Organisations can now block any workflow that references an action without a SHA-pinned version via a repository or organisation ruleset. This is the enforcement layer - it makes the policy machine-verifiable rather than relying on code review.
The Four Controls That Would Have Stopped This
1. SHA pinning with automated updates. Pin every action to a commit SHA. Use Dependabot or Renovate to open PRs when new releases ship, updating the SHA. This preserves the security property while removing the maintenance burden.
2. Minimal token permissions. Set permissions: read-all at the workflow level and grant write permissions only where explicitly needed. The reviewdog compromise only had downstream impact because the victim workflows were running with broad default permissions.
permissions:
contents: read
pull-requests: write # only if needed
3. OIDC instead of stored secrets. The attack dumped GITHUB_TOKEN and other secrets from runner memory. For AWS, GCP, and Azure integrations, replace stored credentials with OIDC federation. Short-lived tokens issued per-job have nothing to dump that is useful beyond the job’s lifetime.
4. Runtime monitoring. StepSecurity’s Harden-Runner agent monitors network egress and file system access during workflow execution. An action exfiltrating memory to an external endpoint triggers an alert even if the action code looks clean at review time. The tj-actions payload exfiltrated to 104.21.14.120 - a network policy blocking unexpected egress domains would have killed the exfiltration before the stolen credentials were transmitted.
GitHub’s 2026 Roadmap Response
GitHub published a 2026 Actions security roadmap directly referencing the tj-actions incident. The items with the most practical impact:
Workflow dependency locking - A dependencies: section in workflow files that locks all transitive action SHAs, analogous to package-lock.json. Targeting public preview mid-2026. This would automate what SHA pinning does manually and extend it to transitive dependencies (actions that call other actions).
Immutable releases - Replacing the mutable tag model entirely for Marketplace actions. A published release becomes a content-addressed object that cannot be overwritten. The tag can still be moved, but the Marketplace entry will refer to the immutable SHA.
Scoped credentials - Removing the secrets: inherit footgun from reusable workflows. Callers will need to explicitly pass each secret, making the blast radius of any credential compromise visible in the workflow YAML.
The Forensic Value of Build Provenance
One of the quieter lessons from tj-actions is the value of SLSA build provenance as a forensic tool. Artifacts built and signed with attest-build-provenance before the compromise window carry valid Sigstore provenance records. Artifacts built during the attack window do not - or carry provenance signed by the attacker’s compromised credential.
This gives security teams a forensic timestamp without having to reconstruct runner logs. gh attestation verify against an artifact tells you whether the build chain was clean at the moment of signing.
The attack would still have happened. But the blast radius would have been measurable and containable.
Immediate Actions
If you run any GitHub Actions workflows today:
- Run
grep -r "uses:" .github/workflows/ | grep -v '@[0-9a-f]\{40\}'to find all unpinned actions. - Replace each with the SHA of the current release. GitHub’s UI shows the commit SHA on every release page.
- Add
permissions: read-allto every workflow that does not need write access. - Enable push protection and secret scanning on every repository.
- Consider Harden-Runner for any workflow that handles sensitive credentials.
The attack was not sophisticated. It relied on standard Git operations against a configuration almost every organisation has. That is precisely what makes it a blueprint rather than a one-off incident.