Vigilant Research

The Software Supply Chain Crisis — 74.5% of Findings Are Unpinned Actions

Written by Chris Nyhuis | Mar 24, 2026 6:14:52 PM
Chris Nyhuis
CEO, Vigilant
10 min read
Part of a 9-part research series on CI/CD pipeline security. See all articles below.

Three-quarters of every CI/CD security finding in GitHub’s top 50K repos comes down to a single mistake: trusting a version tag that anyone with push access can silently move.

The Setup

When we scanned 50,012 of GitHub’s most-starred repositories for CI/CD vulnerabilities, one rule dominated everything else. RGS-007 - unpinned third-party actions - accounted for 143,616 of 192,776 total findings. That’s 74.5%. Not a plurality. A supermajority.

The finding is deceptively simple. A GitHub Actions workflow references a third-party action using a mutable version tag - uses: actions/checkout@v4 - instead of pinning to an immutable SHA hash - uses: actions/checkout@b4ffde65f46.... The version tag @v4 is a Git tag. Any maintainer with push access to that repository can move it to point at different code. The SHA hash is a specific commit. It cannot be changed.

This distinction - mutable versus immutable - is the fault line that the entire CI/CD supply chain runs across. 19,005 repositories in our dataset are on the wrong side of it.

What Mutable Tags Actually Mean

When you write uses: docker/login-action@v3 in a workflow, you’re making a trust decision. You’re saying: “I trust that whoever controls the v3 tag on the docker/login-action repository will only ever point it at code I’d approve.” That trust extends indefinitely, through every future modification of that tag, without any notification or approval process on your end.

A SHA pin - uses: docker/login-action@74a8a23... - says something different. It says: “Run this exact code, this exact commit, nothing else.” If the action maintainer pushes a new version, your workflow keeps running the code you reviewed. You upgrade deliberately, not automatically.

The difference matters because of how GitHub Actions executes workflows. When a CI pipeline triggers, GitHub resolves the action reference at runtime. A mutable tag resolves to whatever commit it points to right now. If an attacker compromises the maintainer’s account and moves the tag to a malicious commit at 2 AM, every workflow that triggers after that moment runs the attacker’s code. No pull request. No code review. No notification.

This is exactly what happened with tj-actions/changed-files in March 2025. A maintainer account was compromised, the tag was moved to a malicious commit, and every repository using the action ran the compromised code on their next CI trigger. The attack extracted CI secrets - GITHUB_TOKEN, AWS credentials, NPM tokens - from every affected pipeline.

The Most Commonly Unpinned Actions

Here’s where 19,005 repositories are placing their trust:

Action Repos Findings
docker/login-action@v3 1,848 5,099
docker/setup-buildx-action@v3 1,845 4,258
softprops/action-gh-release@v2 1,405 2,065
ruby/setup-ruby@v1 1,275 2,793
docker/setup-qemu-action@v3 1,181 2,038
shivammathur/setup-php@v2 1,147 2,916
docker/build-push-action@v6 1,090 2,665
dtolnay/rust-toolchain@stable 989 3,805
codecov/codecov-action@v5 947 1,447
docker/metadata-action@v5 900 1,734

Docker actions dominate the top of the list - five of the top ten most commonly unpinned actions belong to a single organization. We explore this concentration risk in depth in The Docker Chokepoint.

But the pattern below Docker is what reveals the structural problem: action-gh-release, setup-ruby, setup-php, codecov-action, rust-toolchain. Several of these are maintained by single GitHub accounts - one person standing between the open-source ecosystem and a supply chain compromise.

The Trust Paradox in the Supply Chain

The organizations developers trust most aren’t immune to this problem - they’re often the worst offenders. In our pillar research, we found that a major Java framework organization has a 92.9% vulnerability rate - 26 of 28 repos running unpinned actions. A major OSS foundation sits at 65.1% across 172 repos. A major social media company at 66.2%.

This isn’t negligence. It’s a function of scale. Larger organizations run more complex CI/CD - multi-stage builds, matrix testing across platforms, release automation with credential access, deployment pipelines spanning multiple cloud providers. Each additional build step adds action dependencies. Each dependency is another mutable trust decision.

The counterintuitive result: organizations with the resources to do security well have the most exposed CI/CD pipelines precisely because they have the most complex CI/CD pipelines. The brand name on a GitHub organization isn’t a security guarantee for the actions it produces - or consumes.

The Single-Maintainer Problem

Beyond Docker (an organization with corporate security practices), the biggest supply chain risks in our dataset trace to actions maintained by a single GitHub account. One compromised account equals hundreds of compromised repositories:

Action Repos Affected Versions Role
action-gh-release 1,405 14 The dominant release publishing action
setup-php 1,147 8 The PHP setup action
rust-toolchain 989 44 The dominant Rust CI dependency
create-pull-request 353 59 Popular PR automation action
rust-cache 198 8 Rust build cache action
release-action 136 8 Alternative release publishing

To be clear: the maintainers behind these actions have built critical infrastructure that the open-source ecosystem depends on daily. That work is often unpaid, under-recognized, and carried out over years of sustained effort. The problem isn’t the people - it’s the structural reality that thousands of CI/CD pipelines depend on a single account with no organizational redundancy, no succession plan, and no security budget. The ecosystem built a single point of failure around their generosity.

action-gh-release is the most popular release publishing action on GitHub. 1,405 repositories in our dataset use it with mutable tags - all maintained by a single account. A single compromised credential would inject malicious code into the release pipelines of over a thousand of the world’s most popular projects. The release pipeline is where binaries get built, signed, and published - it’s the highest-value target in the entire CI/CD chain.

rust-toolchain is effectively Rust’s CI infrastructure. 989 repos depend on it across 44 unique mutable references - @stable, @master, @nightly, @v1, plus dozens of specific version strings. 201 repos pin to @master, meaning every single push to the repository immediately executes new, unreviewed code in 201 CI pipelines. Combined with rust-cache at 198 repos, two single-maintainer actions control the CI pipelines of most Rust open-source projects.

create-pull-request spans 59 unique action references across 353 repos, with version sprawl from v3 through v8. It appears in 203 repos across 6 major versions - all unpinned, all mutable.

These aren’t hypothetical risks. The tj-actions/changed-files compromise in March 2025 was a single-maintainer action. The attack vector was the maintainer’s account, not a software vulnerability. The action’s code was fine. The credentials weren’t.

Version Sprawl - The Hidden Multiplier

The attack surface for a single action isn’t one version tag. It’s dozens.

Action Unique Refs Top Versions Worst Ref
dtolnay/rust-toolchain 44 @stable (481), @master (201), @nightly (115) @master - 201 repos on HEAD
codecov/codecov-action 35 @v5 (500), @v4 (204), @v3 (138) 35 separate mutable trust points
docker/build-push-action 28 @v6 (610), @v5 (291), @v4 (128) @master - 3 repos + EOL versions

An attacker doesn’t need to compromise the latest version. Any mutable ref is a valid target. Old versions are less monitored but still actively used - 85 repos still run codecov/codecov-action@v1. And refs that look specific, like @v5.5.2, create an illusion of pinning. They’re still mutable tags, not SHAs. The version number is a label, not a guarantee.

No maintainer is tracking or auditing all 28 to 44 mutable refs to their action. A compromised old tag could go unnoticed for months.

The @master/@main Problem

Version tags like @v3 can theoretically be moved, but in practice rarely are outside of an attack. Branch refs like @master and @main move with literally every commit. They are the most dangerous form of unpinned action reference - zero stability guarantee, maximum exposure.

Action Repos on @master/@main Risk
dtolnay/rust-toolchain 201 Rust ecosystem - changes with every push, single person
ad-m/github-push-action 49 Pushes code - direct write access to downstream repos
google/oss-fuzz (run_fuzzers) 44 Security fuzzing infrastructure
google/oss-fuzz (build_fuzzers) 43 Security fuzzing infrastructure
jlumbroso/free-disk-space 41 Runs as root to free disk space
coverallsapp/github-action 40 Code coverage - accesses repo content
aquasecurity/trivy-action 19 Vulnerability scanner pinned to master

ad-m/github-push-action@master deserves special attention. 49 repos use it to push code directly to their own repository. This action has write access by design - it exists to modify your repo. Pinned to master. Every commit to that action’s repository immediately executes in 49 of GitHub’s top projects with full write access to those projects’ code.

Security Scanners Running Unpinned

The most ironic finding in the dataset:

Action Repos Pinned To
google/oss-fuzz 44 @master
coverallsapp/github-action 40 @master
aquasecurity/trivy-action 19 @master
trufflesecurity/trufflehog 8 @main

Trivy - Aqua Security’s vulnerability scanner - and TruffleHog - a secret scanner - are pinned to @master and @main respectively. Tools designed to detect security issues, deployed in the most insecure way possible. A compromise of either repository would inject malicious code into the security scanning step of dozens of top repos. The scanner becomes the attack vector.

The Recursive Supply Chain

We identified 187 repos in our dataset that are themselves GitHub Actions - installable components consumed by other repositories’ workflows. 145 of them - 77.5% - have their own CI/CD vulnerabilities.

This creates a recursive supply chain problem. You might SHA-pin the action you depend on, but if that action’s own CI/CD pipeline uses unpinned dependencies, the chain of trust has no foundation. Compromising one of these foundational tools doesn’t just affect that repo - it affects every Action built and tested with it, and every repo that uses those Actions.

It’s turtles all the way down. The full chain attack anatomy shows how each link in this recursive chain becomes an entry point for exploitation.

New-Gen Tooling - Already Unpinned

The newest generation of developer tooling is repeating every mistake:

Action Total Repos Growth Signal
astral-sh/setup-uv 276 Python’s fastest-growing package manager
oven-sh/setup-bun 195 Bun runtime setup
anthropics/claude-code-action 175 AI code review - nearly doubled during our scan

astral-sh/setup-uv has accumulated 276 unpinned references across 12 different version tags in the short time since uv exploded in the Python ecosystem. oven-sh/setup-bun is at 195 repos with zero SHA pinning. The ecosystem hasn’t learned from Docker, codecov, or dtolnay - every new tool that gains traction immediately becomes an unpinned supply chain dependency.

anthropics/claude-code-action is particularly notable - it nearly doubled from 77 repos at our 40% scan mark to 175 at completion. AI tools in CI/CD are being adopted faster than security awareness can follow. The implications of AI agents in CI/CD pipelines - and the novel attack surfaces they create - are covered in AI Agents as Force Multipliers.

The Zombie Supply Chain

Not every repo can be fixed. 809 archived repositories in our dataset have vulnerabilities that will never be patched - nobody’s home to accept a pull request. But they still get forked, cloned, and referenced. LibreSpark/LibreTV has 26,944 forks, each carrying 30 vulnerable workflow findings. solana-labs/solana is archived with 4 critical findings and 5,557 forks.

26,944 forks x 30 findings + 5,557 forks x 4 findings = 830,548 inherited vulnerabilities from just two archived repositories that will never receive a patch.

Beyond archived repos, 376 repositories haven’t been pushed to in over a year. 129 haven’t seen activity in two years. These are zombie pipelines - abandoned infrastructure that the ecosystem still trusts, with supply chain dependencies that will never be updated. The long tail of this problem is explored in What’s Next - Fixing 50K Repos.

Why Dependabot Isn’t Solving This

GitHub ships Dependabot with built-in support for SHA pinning GitHub Actions. Adding a few lines to .github/dependabot.yml configures automatic SHA pin updates. The tooling exists. The adoption is near zero.

The reasons are structural, not technical. SHA-pinned action references are ugly - a 40-character hex string instead of a clean @v4. Developers prioritize readability. GitHub’s own documentation examples use version tags, not SHA pins. The starter-workflows repository - the templates new users clone - uses version tags. The ecosystem teaches the insecure pattern by default.

Runner Guard’s autofix engine resolves this mechanically: it replaces every mutable tag with its current SHA, adds a version comment for readability, and outputs a workflow file that looks almost identical but is now immutable. The Fix It guide walks through the complete process.

What You Can Do About It

  1. SHA-pin every third-party action. Run runner-guard scan . to find every unpinned reference, then runner-guard fix . to resolve them to SHA pins automatically.

  2. Configure Dependabot for GitHub Actions. Add github-actions to your .github/dependabot.yml ecosystem list. Dependabot will submit PRs when new versions are available, keeping your SHA pins current without manual tracking.

  3. Audit your single-maintainer dependencies. If your CI/CD pipeline depends on an action maintained by a single person, treat that as a risk factor - not a deal-breaker, but something that belongs in your threat model.

  4. Eliminate @master/@main references immediately. These are the highest-risk action references in the ecosystem. At minimum, pin to a version tag. Ideally, pin to a SHA.

  5. Watch for version sprawl. If your organization uses the same action across multiple repos with different version tags, consolidate. Each unique mutable ref is a separate trust decision that nobody is tracking.

The supply chain crisis isn’t a technology problem. The fix exists and takes minutes. It’s an awareness problem - 143,616 findings across GitHub’s top 50K repos, and the solution is a command that runs in under a second.

But here’s the uncomfortable truth: a one-time fix isn’t enough. You SHA-pin today, and tomorrow a developer adds a new action with a mutable tag. The supply chain degrades continuously. Point-in-time scanning catches the current state. Continuous monitoring catches the drift. The supply chain crisis will keep recurring until the ecosystem shifts from reactive scanning to continuous pipeline security - treating CI/CD configurations with the same rigor we apply to production infrastructure.

Scan your repos today. Runner Guard is Vigilant’s free, open-source CI/CD security scanner - the same tool that powered this research. Install it in under a minute:

brew install Vigilant-LLC/tap/runner-guard
runner-guard scan github.com/owner/repo

14 security rules. Zero configuration. One command.

Chris Nyhuis
CEO, Vigilant

CEO of Vigilant, a global cybersecurity firm he has led for 16 years. 30+ years of experience across offensive security, SCADA/IoT, and critical infrastructure defense. Holds multiple patents including Forensically Validated Detection Systems and Secure Protocol Translation. Former instructor at a US intelligence school. Certified human trafficking investigator and OSINT practitioner. Vigilant dedicates 25% of profits to combating human trafficking, child exploitation, and supporting orphan care worldwide.

Subscribe to get Vigilant's latest security research delivered to your inbox.