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.
Example — FAIL
on: pull_request
jobs:
  build:
    runs-on: [self-hosted, linux]   # fork PR code runs on your own host
Example — PASS
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. --ephemeral registration / 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.

  • See Self-hosted runner security.

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