Self-hosted runner exposed to untrusted fork code
ID |
self_hosted_runner |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Family |
SCM |
Tags |
ASVS50:v13.1.1, cicd-sec-07, cicd-security, infrastructure, reachable, security, spvs10-v3.1.1, supply-chain |
Description
Does a GitHub Actions job run on a self-hosted runner in a workflow that untrusted fork code can trigger?
Self-hosted runners execute jobs on infrastructure you own. Unlike GitHub-hosted runners (a fresh, disposable VM per job), a self-hosted runner is non-ephemeral by default and shares the host across jobs. If a workflow triggered by pull_request (or pull_request_target) runs on a self-hosted runner, a malicious fork pull request can execute arbitrary code on your infrastructure, read what previous jobs left behind, install persistence, and pivot into your network (OWASP CI/CD Top-10 CICD-SEC-7, SPVS V3.1 build-environment hardening). GitHub explicitly recommends not using self-hosted runners with public-repository pull_request workflows.
A job is considered self-hosted when its runs-on includes the self-hosted label (a string or an array entry). The detector reports one finding per such job in a fork-exposed workflow, anchored to the runs-on: line.
With flagAllSelfHosted: true the detector also emits a lower-severity inventory finding for every self-hosted job (regardless of trigger), to prompt a review of runner ephemerality and runner-group restrictions — those are runner/organization settings not visible in the workflow file, so they must be verified manually. GitHub-Actions only.
|
on: pull_request
jobs:
build:
runs-on: [self-hosted, linux] # fork PR code runs on your own host
on: pull_request
jobs:
build:
runs-on: ubuntu-latest # disposable GitHub-hosted runner
Security
The safe posture: run untrusted (fork pull_request) workflows only on ephemeral GitHub-hosted runners. Reserve self-hosted runners for trusted events (push to protected branches, workflow_dispatch, deployments), make them ephemeral (one job then torn down), and scope them to a runner group limited to specific repositories. This bounds the blast radius of a compromised job to a single short-lived host.
Mitigation / Fix
-
Move fork-exposed workflows (
on: pull_request) to GitHub-hosted runners (runs-on: ubuntu-latest, etc.). -
If self-hosted runners are required, make them ephemeral (e.g.
--ephemeralregistration / just-in-time runners) so no state survives a job. -
Restrict self-hosted runners to a runner group scoped to trusted repositories, and never enable them for public repositories that accept fork PRs.
Configuration
To change these options you can modify SCANNER_DIR/conf/misconfigurations/self_hosted_runner.yml. The following are the default configuration properties:
properties:
# If true, also emit a lower-severity inventory finding for every self-hosted job (regardless of
# trigger), to prompt review of runner ephemerality and runner-group restrictions.
flagAllSelfHosted: false