Skip to content
Français
CI/CD & Automatisation medium

Supply chain attacks on GitHub Actions: real cases and protections

45 min de lecture

Read this page in French

This guide lets you understand and block the supply chain attacks that compromised tens of thousands of repositories in 2025-2026 through GitHub Actions. You will learn to recognise the exploitation vectors, audit your existing workflows, and deploy defence in depth. Prerequisites: knowing the basics of GitHub Actions and having read the GitHub Actions security guide.

What you will learn

  • Recognise the 5 vectors exploited against GitHub Actions
  • Take apart the real attacks of 2025-2026: tj-actions, GhostAction, Shai Hulud v2, hackerbot-claw
  • Audit an existing repository looking for vulnerable configurations
  • Fix each vector: pull_request_target, mutable tags, permissions, injection, exfiltration
  • Deploy defence in depth across five complementary layers

What you actually risk

A badly configured GitHub Actions workflow gives an attacker privileged access to your whole delivery chain. This is not theoretical: in 2025, real attacks exploited these weaknesses to steal secrets, inject malicious code and compromise packages distributed to thousands of users.

Here is what an attacker can obtain by compromising a single workflow:

ResourceImpactReal case
Secrets (tokens, credentials)Theft and reuse to reach your cloud systems, registries, APIsGhostAction: 3,325 secrets stolen (AWS, PyPI, npm)
Source codeInvisible backdoors injected into the codeShai Hulud v2: a worm replicating into published packages
Artefacts (images, binaries)Malware distributed to your usersNx s1ngularity: infected npm packages
Registries (npm, Docker Hub)Compromised versions published under your name1,092 npm versions infected by Shai Hulud v2
ProductionMalicious code deployedCloud access through stolen tokens

Anatomy of the 2025-2026 attacks

tj-actions/changed-files, March 2025

The most popular action for detecting changed files (23,000+ repositories). An attacker obtained the PAT (Personal Access Token) of a bot holding write access to the repository, most likely through the compromise of another action, reviewdog/action-setup (CVE-2025-30154). They then rewrote the existing tags, from v1 to v45.0.7, to point at a malicious commit that exfiltrated every secret into the public logs of the workflow. The incident is tracked as CVE-2025-30066.

Vectors exploited:

  • Mutable Git tags (users trusted @v1)
  • No pinning by SHA
  • Secrets visible in the workflow logs

The lesson: a @v1 tag is only a moving label. Only a SHA guarantees that the code executed is the code you reviewed.

GhostAction, September 2025

327 accounts compromised, 817 repositories infiltrated, 3,325 secrets stolen. The method rested on no workflow flaw: the attackers first took over maintainer accounts, then read the legitimate workflows to inventory the names of the secrets available in each repository. They then committed a malicious workflow disguised as "Github Actions Security", which copied those secrets by name and sent them through an HTTP POST request to a collection point. The stolen credentials covered AWS keys, PyPI, npm, DockerHub and GitHub tokens.

Vectors exploited:

  • Maintainer account compromise (no robust second factor)
  • No branch protection on .github/workflows/
  • No mandatory review on adding a workflow file
  • No control over outbound network connections from the runner

Shai Hulud v2, November 2025

A self-replicating worm at scale on the npm ecosystem. Datadog's analysis counts 796 unique npm packages carrying the worm, that is 1,092 versions published, and more than 14,000 GitHub repositories created for exfiltration; other counts go beyond 25,000 repositories. Propagation does not go through the workflows: the worm injects two files (setup_bun.js and bun_environment.js) triggered by a preinstall script, harvests the credentials present on the machine, then uses the stolen npm tokens to automatically republish other booby-trapped packages with npm publish.

The link to GitHub Actions is real but indirect. On compromised machines, the worm installs a self-hosted runner and deploys a deliberately vulnerable action on it: the attacker then takes back control by opening a GitHub Discussion containing the commands to run, which gives them a command and control channel with no external server. The worm also injects a shai-hulud-workflow.yml workflow that exfiltrates the secrets on every push.

Vectors exploited:

  • npm preinstall scripts run without a sandbox at install time
  • Long-lived npm tokens and GitHub PATs stored in clear text on workstations
  • Registration of an unmanaged self-hosted runner
  • No network control (free exfiltration)

hackerbot-claw / Trivy, February-March 2026

An autonomous AI bot that targeted 7 major open source repositories between 20 February and 2 March 2026. The bot identified vulnerable workflows automatically, opened malicious pull requests, and exploited, depending on the target, either pull_request_target with a checkout of the fork, or a command injection through the branch name or a file name. Every exploitation delivered the same remote payload.

On Trivy, the attack led to the theft of a maintainer's PAT, then to the complete wiping of the GitHub repository and the deletion of every release from v0.27.0 to v0.69.1 during the night of 1 March 2026.

Vectors exploited:

  • pull_request_target with no guards
  • GITHUB_TOKEN holding write permissions
  • A workflow that ran code coming from the fork
  • ${{ }} interpolation of branch and file names inside a run: block
  • No author_association check on commands sent through comments

For the full account of the incident, see the post Trivy wiped after a supply chain attack by an AI bot.

The 5 main attack vectors

These four incidents do not share an entry point: two go through a vulnerable workflow, two through a stolen credential. But all of them then pass through the same doors, and those 5 vectors are the ones you can close in your own repositories. Fixing them does not make you invulnerable to a stolen PAT, but it removes nearly all of the externally exploitable surface and strongly limits what a stolen credential yields.

Vector 1, the pwn request (pull_request_target)

This is the most devastating vector. The pull_request_target event was designed to let maintainers react to fork pull requests with access to the secrets (to label them, for instance). The problem: it runs the workflow in the context of the target repository, with its secrets and a GITHUB_TOKEN potentially holding write rights.

EventCode executedAccess to secretsRisk
pull_requestThe fork codeNo (forks)Low
pull_request_targetThe target branch codeYesCritical if the fork is checked out

The exploitation scenario:

  1. The attacker forks the target repository

  2. They edit a script (build, test, linter) to inject malicious code

  3. They open a pull request, which triggers pull_request_target

  4. The workflow checks out the pull request code (ref: ${{ github.event.pull_request.head.sha }}) and runs it with the privileges of the target repository

  5. The malicious code exfiltrates the secrets, modifies the code, or spreads

How to protect yourself:

# ❌ DANGEROUS: fork code checked out with access to the secrets
on:
pull_request_target:
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm test # Runs the fork code with the repository secrets

The safe version switches to pull_request: forks then have no access to the secrets.

# ✅ SECURED: use pull_request (no access to secrets for forks)
on:
pull_request:
types: [opened, synchronize]
jobs:
test:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm test

If you absolutely need pull_request_target (which is rare), apply these guards:

  • Never check out the pull request code
  • Add a restrictive if condition (a specific label, an allowed author)
  • Keep the permissions to the strict minimum

Detailed guide: Securing pull_request_target.

Vector 2, mutable tags and the action supply chain

When you write uses: some/action@v1, GitHub resolves the tag to a commit. But a tag is a moving label: the maintainer (or an attacker) can shift it to any commit at any time.

That is exactly the mechanism tj-actions/changed-files exploited: the @v1, @v2 and other tags were rewritten to point at code that exfiltrated the secrets.

How to protect yourself:

# ❌ Mutable tag: the content can change without notice
- uses: actions/checkout@v4
# ❌ A precise version, but the tag stays mutable
- uses: actions/checkout@v4.2.2
# ✅ Full SHA: immutable and verifiable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  1. Convert every tag into a SHA

    Use StepSecurity (web interface) or the CLI tool:

    Fenêtre de terminal
    npm install -g pin-github-action
    pin-github-action .github/workflows/ci.yml
  2. Enable Dependabot for automatic updates

    .github/dependabot.yml
    version: 2
    updates:
    - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
    interval: "weekly"
  3. Audit with Scorecard

    Fenêtre de terminal
    scorecard --local . --checks Pinned-Dependencies --show-details

Detailed guide: Pinning actions by SHA.

Vector 3, excessive GITHUB_TOKEN permissions

The GITHUB_TOKEN is generated automatically for every workflow. With permissions that are too broad, a compromised workflow can modify the source code, create releases, publish packages, and persist inside the repository.

It is an accelerator for every other attack. As the Arctiq analysis points out, combined with pull_request_target it is an "explosive combination", used by Shai Hulud v2 and GhostAction.

How to protect yourself:

# Zero permissions by default at the workflow level
permissions: {}
jobs:
test:
runs-on: ubuntu-24.04
permissions:
contents: read # Only what is needed
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm test
publish:
needs: test
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write # Write only for the job that publishes
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm publish

Detailed guide: GitHub Actions permissions.

Vector 4, command injection

The ${{ }} interpolation inside a run: block happens before the shell runs, with no escaping. An attacker controlling a pull request title, a branch name or an issue body can inject arbitrary code.

The CVE-2025-53104 flaw in gluestack/gluestack-ui, published in June 2025, illustrates this vector exactly: the discussion-to-slack.yml workflow interpolated the title and body of a GitHub Discussion into a run: block, which let anyone execute code on the runner by opening a discussion containing $(...). It was discovered alongside a separate incident, the compromise of the project's npm packages, which itself came from the theft of a developer's PAT. The vulnerable workflow was deleted.

Dangerous data (never interpolate it directly):

SourceVariable
Pull request titlegithub.event.pull_request.title
Pull request bodygithub.event.pull_request.body
Issue titlegithub.event.issue.title
Commentgithub.event.comment.body
Branch namegithub.event.pull_request.head.ref
Commit messagegithub.event.head_commit.message
Dispatch inputgithub.event.inputs.*

How to protect yourself:

# ❌ Injection possible
- run: echo "PR: ${{ github.event.pull_request.title }}"
# ✅ An environment variable: the shell treats the value as a string
- run: echo "PR: $PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}

Vector 5, uncontrolled network exfiltration

Every preceding vector needs an outbound channel to send the stolen secrets. If you block outbound network connections, even a compromised workflow cannot exfiltrate your data.

How to protect yourself:

steps:
# MUST be the first step of the job
- uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit # Observe first, then block
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: npm ci && npm test
  1. Deploy in audit mode to observe the legitimate connections

  2. Analyse the StepSecurity dashboard (the link appears in the job logs)

  3. Switch to block mode with the list of allowed endpoints

    - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
    with:
    egress-policy: block
    allowed-endpoints: >
    github.com:443
    registry.npmjs.org:443
    objects.githubusercontent.com:443

A defence in depth strategy

No single measure is enough. The security of your workflows rests on the combination of several protection layers:

LayerGoalTools and measures
1, PreventionCatch problems before the commitactionlint, zizmor, poutine, Semgrep
2, ConfigurationReduce the attack surfacepermissions: {}, SHA pinning, OIDC
3, DetectionWatch during the runHarden Runner, Gitleaks, Scorecard
4, ContainmentLimit the blast radiusegress: block, ephemeral runners, gated environments
5, ResponsePrepare for the worstSecret rotation, audit logs, SIEM

Layer 1, prevention: catching problems before the commit

This layer is the cheapest and the one that pays best: it refuses a dangerous workflow before it exists in production. The first four tools in the table do not replace one another, they have distinct angles. actionlint validates the syntax and the expressions, zizmor reasons about the security of the workflow itself, poutine covers several CI platforms and spots pipeline-specific patterns, Semgrep carries your own rules. Chain them in the same job, each failing independently, rather than picking just one.

ToolWhat it detects
actionlintSyntax errors, dangerous patterns
zizmorStatic security audit of the workflows
poutineVulnerabilities specific to GitHub Actions
SemgrepCustom rules (banning pull_request_target, and so on)
CheckovBad practices in the workflows

Layer 2, configuration: reducing the attack surface

These are the measures described in the 5 vectors above. They need no extra tool, only rigour in the YAML, and their effect is cumulative: each one removes a capability from an attacker who has already gained a foothold in the pipeline. The most profitable of the four is OIDC, which removes the whole category of stealable cloud credentials.

  • permissions: {} at the workflow level
  • Systematic SHA pinning
  • OIDC for cloud deployments
  • Secrets scoped per step and environment

Layer 3, detection: watching in real time

The two previous layers assume you thought of everything. This one assumes the opposite and looks for deviations from the expected behaviour while the job runs. It has a start-up cost: every tool produces noise until the baseline is established. Start with Harden Runner in observation mode, let it run for two weeks, and you will hold the list of network destinations your pipelines actually use. That list is what makes layer 4 applicable.

  • Harden Runner: watches the network connections during the run
  • Gitleaks / TruffleHog: continuous scanning of commits for secrets
  • Scorecard: a periodic audit of the security posture

Layer 4, containment: limiting the blast radius

Containment does not try to prevent the compromise but to make it sterile. An attacker running code on a runner with no allowed network egress, destroyed after the job, leaves with nothing. These three measures are laid down in this order: network blocking first, because it cuts exfiltration, the source of value in nearly every attack listed above; manual approval last, because it costs human time on every deployment.

  • egress: block: preventing network exfiltration
  • Ephemeral runners: no persistence between jobs
  • Environments with approval: a human gate before production

Layer 5, response: preparing for the worst

This layer is judged by one question: how long does it take you to revoke and replace every secret of a repository? If the answer is over an hour, or if nobody knows it, you have no response plan. The 2025-2026 attacks show that the useful window is short: the exfiltration of the Trivy secrets and their exploitation fitted into the same night. Write the rotation procedure, test it once, and keep the audit logs somewhere other than the compromised repository.

  • Secret rotation: a documented, tested procedure
  • Audit logs: centralised in a SIEM for investigation
  • Dependabot: automatic alerts on vulnerable actions

A 5-step action plan

If you are starting from scratch, here is where to begin:

  1. Audit your existing workflows

    Fenêtre de terminal
    # List every workflow
    find .github/workflows -name '*.yml' -o -name '*.yaml'
    # Look for dangerous patterns
    grep -rn 'pull_request_target' .github/workflows/
    grep -rn 'permissions:.*write-all' .github/workflows/
    grep -rn '\${{.*github\.event\.' .github/workflows/
    grep -rn '@v[0-9]' .github/workflows/
    # Full audit with Scorecard
    scorecard --local . --format json | jq '.checks[] | {name, score}'
  2. Fix the critical flaws first

    • Remove every pull_request_target that checks out the fork
    • Add permissions: {} to every workflow
    • Replace ${{ github.event.* }} inside run: blocks with env: entries
  3. Pin every action by SHA

    Fenêtre de terminal
    npm install -g pin-github-action
    find .github/workflows -name '*.yml' -exec pin-github-action {} \;
  4. Add Harden Runner and Gitleaks

    Deploy Harden Runner in audit mode on every job, and Gitleaks to block the merge when a secret is detected.

  5. Put continuous monitoring in place

    Enable Dependabot for github-actions, schedule a monthly Scorecard audit, and configure the alerts in your SIEM.

Key points

Supply chain attacks on GitHub Actions boil down to 5 vectors: a badly secured pull_request_target, mutable tags, excessive permissions, command injection and uncontrolled network exfiltration. Those are the five doors you control in your repositories.

The 2025-2026 incidents fall into two families. tj-actions and hackerbot-claw start from a workflow weakness and are therefore avoidable through the five fixes above. GhostAction and Shai Hulud v2 start from a stolen credential, a maintainer account or an npm token: against those, the useful measures are strong authentication, short-lived credentials (OIDC) and protecting the workflow files with mandatory review.

The good news: fixing these 5 vectors needs no sophisticated tooling. It is a matter of discipline: minimal permissions, SHAs everywhere, inputs never interpolated, secrets isolated, network watched.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn