A GitHub Actions workflow is not configuration in the harmless sense. It is code that runs code, often with tokens, secrets, release permissions, cloud access, and the social trust of a green checkmark.
That is the useful lesson in Novee’s June 23, 2026 research on “Cordyceps”: the dangerous part is not one branded vulnerability. It is the repeatable pattern where untrusted repository input crosses into privileged automation and starts acting with maintainer authority.
TL;DR
- Novee reports finding hundreds of exploitable CI/CD attack chains across high-impact repositories, including examples involving Microsoft, Google, Apache, Cloudflare, and the Python Software Foundation.
- Treat “Cordyceps” as a named research finding, not a CVE. The defensive issue is older and broader: workflow trust boundaries, token permissions, secrets exposure, and unsafe handling of untrusted pull request data.
pull_request_targetandworkflow_runare not bad by themselves. They become dangerous when they process attacker-controlled code, artifacts, comments, branch names, titles, or scripts in a privileged context.- The fix is architectural: separate untrusted build/test work from privileged commenting, publishing, signing, and deployment.
- Scanners help, but the real question is still human: “Can an outsider make this workflow do something only a maintainer should be able to do?”
What Novee Found
Novee says its team scanned roughly 30,000 high-impact repositories and flagged 654 repositories in one scan, with more than 300 confirmed as fully exploitable. The reported impact classes included command injection, broken authorization logic, artifact poisoning, cross-workflow privilege escalation, credential theft, package publishing risk, and repository write access.
The named examples are serious: Microsoft Sentinel content, Google’s ADK sample repository, Apache Doris, Cloudflare Workers SDK, and PSF Black. Some of the vendor-specific details are necessarily based on Novee’s disclosure account, so defenders should treat the exact blast-radius language as attributed research unless the affected vendor has published its own advisory.
The pattern, however, is independently familiar. GitHub’s own secure-use guidance warns that pull_request_target and workflow_run workflows can expose repository secrets or write access when they check out or process untrusted pull request content. GitHub Security Lab has been warning about the same “pwn request” class for years.
The uncomfortable part is that these bugs often do not look like bugs. A workflow labels a PR. Another downloads an artifact. A script reads a branch name. A bot posts approval-like comments. Each line can look reasonable. The exploit appears when those pieces form a path from outsider-controlled input to privileged action.
The Trust Boundary That Gets Lost
The core model is simple:
| Source | Trust level | Safe job type |
|---|---|---|
| Fork PR code, branch names, PR titles, comments, issue bodies | Untrusted | Build, lint, test, parse as data |
| Artifacts produced by untrusted jobs | Untrusted | Read as data after validation |
| Repository secrets, signing keys, package tokens, cloud OIDC roles | Privileged | Publish, deploy, sign, comment, modify repo |
GITHUB_TOKEN with write permissions | Privileged | Only in jobs that need that exact write scope |
The failure mode is allowing the first row to influence the third or fourth row without a hard boundary.
A classic vulnerable shape is:
on: pull_request_target:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - run: npm install && npm testpull_request_target runs in the context of the base repository. Checking out the pull request head and running its build scripts can turn a contribution from an outsider into code execution in a more privileged context. The exploit does not need to be clever if the workflow hands it secrets or a write-capable token.
There are subtler versions:
- A PR title, branch name, issue body, or comment is interpolated directly into a shell script.
- An untrusted workflow uploads an artifact, and a privileged
workflow_runjob later executes content from it. - A bot token can approve, label, comment, or update pull requests in ways that bypass human review.
- A workflow has
permissions: write-allbecause it was easier than finding the narrow permissions it needed. - Self-hosted runners process untrusted jobs and keep state, credentials, caches, or network reachability between runs.
This is why “the YAML looks fine” is not enough. You need to trace where attacker-controlled data can flow, what process evaluates it, and which credential is present when that happens.
A Safer Pattern
Use a low-privilege workflow for the untrusted part:
name: Receive PRon: pull_request:
permissions: contents: read
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm test - name: Save result metadata only run: | mkdir -p pr printf "%s" "${{ github.event.number }}" > pr/number - name: Upload metadata uses: actions/upload-artifact@v4 with: name: pr path: pr/Then use a separate privileged workflow only for narrow actions, such as commenting on a PR, and treat any artifact from the first job as hostile input:
name: Comment on PRon: workflow_run: workflows: ["Receive PR"] types: [completed]
permissions: actions: read pull-requests: write
jobs: comment: if: github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest steps: - name: Download metadata uses: actions/download-artifact@v4 with: name: pr path: ${{ runner.temp }}/pr run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Comment through reviewed logic run: | test -f "${{ runner.temp }}/pr/number" echo "Read artifact as data. Do not execute it."This is intentionally boring. Boring is good. The privileged job should not build the PR, run a binary from the PR, source a shell file from the artifact, trust a branch name as code, or publish anything based solely on attacker-controlled output.
What To Audit This Week
Start with workflow triggers:
rg -n "pull_request_target|workflow_run|issue_comment|pull_request_review|workflow_dispatch" .github/workflowsFor each hit, ask:
- Does this job have repository write permissions, secrets, signing keys, package tokens, or cloud access?
- Does it check out PR code, run package scripts, execute artifacts, or evaluate untrusted text in a shell?
- Is
permissions:declared at the top level as read-only, with write permissions granted only per job? - Are third-party actions pinned to full commit SHAs where compromise would matter?
- Are self-hosted runners isolated from public fork PRs and destroyed or cleaned between sensitive jobs?
- Can automation approve, merge, label, or publish without a human review gate?
Then scan for common script-injection shapes:
rg -n "github\.event\.(pull_request|issue|comment|review)|github\.head_ref|github\.ref_name" .github/workflowsFinding a match is not automatically a vulnerability. A PR title passed as an environment variable and treated as data is different from a PR title spliced into an inline shell command. The point is to force review of the dangerous boundary.
Detection Ideas
GitHub Actions compromise often leaves weak signals rather than one obvious alert. Useful detections combine repository audit events, workflow run metadata, runner telemetry, and network logs.
Hunt for:
- New or modified files under
.github/workflows/, especially changes that addpull_request_target,workflow_run, broadpermissions, package publishing, signing, or cloud authentication. - Workflow runs from first-time contributors that reach jobs with write tokens or secrets.
GITHUB_TOKENactivity that creates commits, tags, releases, checks, packages, or PR reviews outside the expected workflow.- Runner processes spawning shells, package managers, cloud CLIs, or network clients from jobs that should only comment or label.
- Outbound connections from CI runners to domains not used by the build, registry, artifact store, cloud provider, or normal telemetry path.
- Artifact downloads in privileged workflows followed by execution, extraction into the workspace, or sourcing of shell files.
For GitHub Enterprise audit logs, build queries around actions such as workflow file changes, secret changes, deploy key changes, release creation, package publishing, branch protection changes, and unusual bot activity. For self-hosted runners, collect process creation and network telemetry the same way you would from a production server. A runner that can publish a package is production infrastructure.
Hardening Priorities
Set the default token posture first:
permissions: contents: readGrant write permissions only at the job that needs them:
jobs: comment: permissions: pull-requests: writeThen remove mixed-trust workflows. If a job builds untrusted PR code, it should not have secrets. If a job has secrets, it should not execute untrusted PR code. If a privileged job consumes artifacts from an untrusted job, validate them as data and keep them out of executable paths.
Use CODEOWNERS for workflow changes. Require review from people who understand CI/CD security, not only application logic. Add OpenSSF Scorecard or equivalent checks for dangerous workflows, token permissions, and pinned dependencies, but do not treat a passing score as a complete review. Scorecard can flag dangerous patterns; it cannot understand every business-specific privilege path.
Prefer GitHub OIDC with tightly scoped cloud roles over long-lived cloud secrets. Restrict OIDC trust policies by repository, branch, environment, workflow, and audience where your provider supports it. A stolen long-lived key is an incident. A narrowly scoped short-lived token is still bad, but the blast radius is smaller.
Finally, inventory your bots. Any token that can approve, merge, publish, comment with authority, or change repository state is part of your supply chain. If an outsider can influence what that bot does through CI, issue comments, labels, artifacts, or generated files, you have a trust-boundary problem.
The Real Lesson
Cordyceps is a useful name because it makes the pattern memorable. But defenders should not wait for a named vulnerability, logo, CVE, or vendor bulletin before fixing it.
The rule is simpler: untrusted repository input must not gain maintainer powers by passing through automation. If an anonymous pull request can make a workflow run code, steal a token, publish an artifact, weaken a check, or speak as a trusted bot, the supply chain is already part of the attack surface.
The green checkmark is not proof of safety. It is proof that automation ran. Make sure the automation knew who it was trusting.
Related Posts
- The Build Is the Target: CI/CD Pipeline Attacks and How to Detect Them - broader CI/CD attack paths and runner-level detection ideas.
- Shai-Hulud: The Open-Source GitHub Actions Token Harvester That Just Went Public - a concrete example of token theft inside GitHub Actions workflows.
- MCP Servers Through an Attacker’s Eyes - the same trusted-automation problem applied to AI tool integrations.