What Happened
In early 2024, an attacker with write access to the tj-actions/changed-files repository modified the GitHub Action to expose encrypted secrets in plaintext within workflow logs. This Action, used by approximately 23,000 repositories, detects changed files in pull requests.
The attacker created orphaned commits—commits not attached to any branch—containing malicious code. They moved release tags like v41 to point at these orphaned commits instead of the legitimate release. When workflows referenced the Action using mutable tags (tj-actions/changed-files@v41), they executed the backdoored version. The malicious code exfiltrated GITHUB_TOKEN and other secrets to attacker-controlled infrastructure.
StepSecurity detected the compromise by monitoring unauthorized network calls from the Action. The attacker's write access allowed them to push commits and move tags without triggering the repository's normal code review process.
Timeline
- Initial compromise date: Not publicly disclosed
- Detection: StepSecurity identified unauthorized network calls from the Action during workflow execution
- Tag manipulation: The attacker moved release tags to orphaned commits containing the malicious payload
- Scope: Approximately 23,000 repositories potentially affected
- Disclosure and remediation: Snyk published analysis and remediation guidance; maintainers restored tags to legitimate commits
Which Controls Failed or Were Missing
1. Mutable Reference Pinning
Workflows referenced Actions using mutable tags (@v41) rather than immutable commit SHAs. When the attacker moved the v41 tag, every workflow using that reference automatically pulled the compromised version.
What should have been in place: Commit SHA pinning. A workflow that referenced tj-actions/changed-files@a1234567890abcdef... would have continued running the vetted code regardless of tag manipulation.
2. Branch Protection for Tag Creation
The repository allowed users with write access to create and move tags without additional approval. This enabled the attacker to push orphaned commits and redirect tags directly.
What should have been in place: Protected tags or branch protection rules requiring pull request reviews before tag creation or modification. GitHub supports ruleset protections for tags, requiring status checks or approvals before tag operations.
3. Network Egress Monitoring
Most affected repositories lacked visibility into network calls made by their Actions. The malicious code sent secrets to external endpoints, but only repositories using StepSecurity's monitoring detected the anomalous traffic.
What should have been in place: Network egress controls that either block or alert on unexpected outbound connections from workflow runners. StepSecurity's Harden-Runner Action provides this capability, but it requires explicit adoption.
4. Secret Exposure in Logs
GitHub encrypts secrets in workflow logs by default, but the attacker's code decoded and logged them in plaintext. The platform's built-in secret masking failed to catch the exfiltration because the secrets were transmitted over the network.
What should have been in place: Runtime secret protection that prevents secrets from being accessed by untrusted code, not just from appearing in logs. This requires either environment-level isolation or secret managers that provide temporary, scoped credentials instead of long-lived tokens.
What the Relevant Standards Require
NIST 800-53 Rev 5
CM-2 (Baseline Configuration): "The organization develops, documents, and maintains...a current baseline configuration of the information system."
Your CI/CD pipeline is part of your information system. Pinning Actions to commit SHAs creates an auditable baseline. Mutable tags violate this control because your "baseline" changes without configuration management approval.
CM-3 (Configuration Change Control): "The organization...determines the types of changes to the information system that are configuration-controlled."
Tag movements are configuration changes. The compromise succeeded because tag operations bypassed change control. Your GitHub repository settings should enforce that tag creation requires the same review process as code merges.
ISO 27001
Annex A 8.32 (Change Management): "Changes to information processing facilities and systems shall be subject to change management procedures."
Pulling an Action by tag means accepting unreviewed changes. Your change management procedure should require that Actions are pinned to reviewed commits, and that updates go through your standard change approval process.
Annex A 8.9 (Configuration Management): "Configurations...of hardware, software, services and networks shall be established, documented, implemented, monitored and reviewed."
Your workflow files are configuration. Document which Action versions you've approved, implement SHA pinning, and review when you update those SHAs.
SOC 2 Type II
CC6.6 (Logical and Physical Access Controls): "The entity implements logical access security measures to protect against threats from sources outside its system boundaries."
Your workflow runners execute third-party code. The trust boundary isn't just your repository — it includes every Action you call. Network egress controls implement this criterion by preventing unauthorized data transmission from your runners.
CC7.2 (System Monitoring): "The entity monitors system components and the operation of those components for anomalies that are indicative of malicious acts, natural disasters, and errors."
You should monitor your workflow executions for unexpected network calls, just as you monitor your production systems. StepSecurity detected this compromise through monitoring; most organizations had no visibility.
Lessons and Action Items for Your Team
Immediate Actions
Pin all Actions to commit SHAs. Run this command in each repository:
grep -r "uses:.*@v[0-9]" .github/workflows/
For each match, replace the tag with the commit SHA that tag currently points to. Use git ls-remote to find the SHA for a tag:
git ls-remote https://github.com/tj-actions/changed-files v41
Update your workflow file:
# Before
uses: tj-actions/changed-files@v41
# After
uses: tj-actions/changed-files@a1234567890abcdef1234567890abcdef12345678
Add a comment with the semantic version for human readability:
uses: tj-actions/changed-files@a1234567890abcdef1234567890abcdef12345678 # v41
Enable Dependabot for Actions. Create .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Dependabot will open pull requests when Action maintainers publish new commits, giving you a review process for updates.
Within 30 Days
Implement network egress monitoring. Add StepSecurity's Harden-Runner to the start of every job:
steps:
- uses: step-security/harden-runner@... # pin to SHA
with:
egress-policy: audit # start with audit mode
Review the audit logs for two weeks. Identify legitimate network calls (package registries, artifact storage, deployment targets). Then switch to egress-policy: block with an allowed endpoints list.
Protect your tags. In your repository settings, create a tag protection rule. For repositories using GitHub Enterprise, create a ruleset that requires pull request approval before tag creation or modification.
Audit your GITHUB_TOKEN permissions. The default token has write access to your repository. Reduce it to read-only in each workflow:
permissions:
contents: read
Grant additional permissions only to jobs that need them:
jobs:
deploy:
permissions:
contents: read
deployments: write
Strategic Changes
Treat Actions as third-party dependencies. Your approval process for adding a new npm package should also apply to adding a new Action. Require that someone on your team reviews the Action's source code before you reference it.
Build an internal Action registry. For Actions your team uses frequently, fork them to an internal organization. Pin your workflows to your forks. This gives you control over updates and a place to apply your own security patches.
Separate workflow secrets from code secrets. Don't use GITHUB_TOKEN for production deployments. Use OIDC to exchange the GitHub token for a short-lived credential from your cloud provider. This limits the blast radius if a workflow token is compromised — the attacker gets a credential that expires in minutes, not a long-lived secret.
The changed-files compromise succeeded because of a gap between how developers think about Actions (convenient tools) and what they actually are (third-party code running in your CI environment with access to your secrets). Close that gap by treating your workflow definitions with the same rigor you apply to your application code.



