Security

Self-hosted runner abuse

Self-hosted runner abuse: an attacker forks a public repo using a self-hosted GitHub Actions runner, opens a PR, and gets remote code execution on the runner's host — often the maintainer's own infrastructure.

Why self-hosted runners exist

GitHub Actions ships hosted runners (ubuntu-latest, macos-latest, windows-latest) for free up to a quota. Beyond that, or for specific hardware needs (GPU, ARM, larger memory, custom OS images), maintainers add self-hosted runners — VMs they own that register with GitHub and pick up jobs.

Self-hosted runners are explicitly supported and common in large orgs. The danger arises specifically when a self-hosted runner is attached to a PUBLIC repository that anyone can fork and PR.

The attack

Attacker forks a public repo whose CI uses runs-on: self-hosted. Opens a PR that modifies a script the CI executes — e.g. tweaks package.json's test script to run arbitrary code, or adds a new test file.

When GitHub picks up the PR, the workflow runs on the maintainer's self-hosted runner. The attacker's modified code executes on the maintainer's infrastructure with whatever filesystem and network access that runner has.

Worst-case: the runner has network access to internal services, AWS metadata endpoints, cloud build credentials, or persistent state on disk. The attacker can pivot from runner to broader infrastructure.

Why GitHub's docs explicitly warn about this

GitHub's documentation on self-hosted runners is unusually direct: 'We recommend that you only use self-hosted runners with private repositories.' This warning has been there since 2020.

The reason it's so direct: there's no clean technical mitigation. Self-hosted runners aren't sandboxed by default. The runner's process has shell access. Code that executes during a fork PR's workflow can do anything the runner's UID can do on that host.

GitHub's hosted runners are sandboxed (ephemeral VM per job, destroyed after). Self-hosted runners are whatever you make them — and most teams don't go through the effort of making them ephemeral.

Detection: how a static rule catches this

Two signals in the workflow YAML: runs-on: self-hosted (or a runner-group label that maps to self-hosted) AND on: pull_request (or pull_request_target, which is even worse — see the pull_request_target attack glossary entry).

LGTM Security flags this combination at PR review time. Rule id: workflow.self-hosted-runner-on-public-repo. The detector checks if the repo is public via GitHub API and only fires when the runner-public-fork combination is present.

Same pattern detected by OSSF Scorecard (Dangerous-Workflow check), GitHub Advanced Security's actions analysis, and several custom Conftest policies in the security-tooling community.

Safer patterns if you need self-hosted runners on a public repo

Option A — Ephemeral runners. Configure the self-hosted runner to spin up a fresh VM per job and destroy it after (GitHub's official 'ephemeral' setting). Even if attacker code runs, it doesn't get persistent access. Use this with a runner image you build yourself, not a long-lived host.

Option B — Restrict to trusted users. Use the workflow_run event with a manual approval step from a maintainer, or branch-protection rules that gate workflow execution behind a label that only maintainers can apply.

Option C — Use a private repo + GITHUB_TOKEN sync. The CI runs in a private repo with self-hosted runners; the public repo just maintains source. Doable but operationally complex.

Option D — Switch to GitHub's hosted runners. If the only reason for self-hosting is cost, the math sometimes flips once you factor in the security ops time. Hosted runners with the new ubuntu-22.04 + larger tiers cover more use cases than they used to.

Examples

Vulnerable workflow (public repo)
name: Build
on: pull_request
jobs:
  build:
    # ❌ DANGER — self-hosted runner on public repo
    # Anyone forking can run arbitrary code on this host
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: ./build.sh   # attacker controls this file in their PR
Mitigation — ephemeral runner + branch gate
name: Build
on:
  pull_request:
    types: [labeled]
jobs:
  build:
    # Only fires when a maintainer adds the 'ci-approved' label
    if: contains(github.event.pull_request.labels.*.name, 'ci-approved')
    runs-on: [self-hosted, ephemeral-builder]
    steps:
      - uses: actions/checkout@v4
      - run: ./build.sh

See LGTM Security's runner-abuse detector

Per-rule policy · merge-block · zero LLM cost

Go to the product page

FAQs

Can attackers escape an ephemeral runner sandbox?

Possible but much harder. Ephemeral runners are a clean VM per job that gets destroyed afterwards. Compromising one gets the attacker access only to whatever the runner has during that single job — no persistent state, no other concurrent jobs to pivot to. Combine with network egress filtering and the blast radius shrinks further.

Is the same risk present with private repos?

Lower, because only collaborators can open PRs in a private repo. But the risk isn't zero — a compromised collaborator account or a forked-private-to-public lifecycle change can still surface the issue. Most maintainers reserve self-hosted runners for private repos with trusted collaborators, which is the right baseline.

What's the difference between this and pull_request_target attacks?

Pull_request_target attacks: workflow runs in base repo VM with secrets, executes fork code. Self-hosted runner abuse: workflow runs on YOUR hardware (which may or may not have secrets), executes fork code. Both end with attacker-controlled code in your environment; self-hosted attacks add the persistent-host pivot risk that hosted-VM attacks don't have.

Can I detect after-the-fact that this happened to me?

Check runner logs for jobs that ran on PRs from fork accounts you don't recognise. If your runner has audit logging on shell history, check that too. The hard part of detection is that the attack often looks like a normal workflow run until the malicious code does something noisy (network exfil, file modification outside the workspace) — which a careful attacker won't do.

Related across LGTM

Related terms