Security Advisoryesp-rs/espflash#1074SEVERITY: Critical

Arbitrary Code Execution on esp-rs/espflash Self-Hosted Runners via Untrusted Fork Pull Requests

TARGET REPOSITORYesp-rs / espflash
PATCHED VERSIONSPR #1074 (HIL slash command gates & runner isolation)
CWE CLASSIFICATION:CWE-284: Improper Access Control · CWE-306: Missing Authentication for Critical Function
Sep - Oct 2026•10 min read•Madhan Alagarsamy
Official Issue & Patch
Arbitrary Code Execution on esp-rs/espflash Self-Hosted Runners via Untrusted Fork Pull Requests

Executive Summary

The esp-rs/espflash repository maintains a non-ephemeral fleet of physical self-hosted runners (BrnoRPIRS01–BrnoRPIRS13, BrnoVMRS06) dedicated to running Hardware-In-the-Loop (HIL) tests against real connected ESP32 SoC devices. The primary automated workflow (.github/workflows/hil.yml) triggered on pull_request events against the base repository, executing on runs-on: [self-hosted, linux, ARM64, ...]. Because GitHub Actions runs workflow definitions from the pull request head (i.e., from the contributor's fork), and repository policies lacked approval requirements for fork pull-request workflows, any attacker could push arbitrary workflow commands that executed directly on Espressif's self-hosted runners without maintainer review or authorization.

Coordinated Disclosure Timeline

September 13, 2026:

Discovered self-hosted runner execution vulnerability; verified via GitHub REST API that 82 of 82 recent fork PR runs executed with zero approval gating.

September 13, 2026:

Submitted private vulnerability disclosure to esp-rs maintainers detailing unauthenticated remote code execution on the Brno self-hosted runner cluster.

September 13, 2026:

Executed live, non-destructive PoC (PR #1070, run 34771920489) using a neutral provenance marker confirming unprivileged command execution on self-hosted host CI17045-RPI.

Late September 2026:

Maintainer (@SergioGasquez) confirmed the vulnerability: 'The vulnerability report has been addressed with #1074 and some GH options. Thanks for opening the report!'

Late September 2026:

Pull Request #1074 ('Add slash commands for HIL testing') merged into main by @MabezDev, establishing maintainer slash-command gates (/hil, /trust) and fork approval controls.

01.Self-Hosted Runner Fleet Architecture & Non-Ephemeral Hosts

To validate firmware flashing across diverse ESP32 SoC architectures, esp-rs/espflash maintains a self-hosted runner cluster of Raspberry Pi single-board computers (BrnoRPIRS01 through BrnoRPIRS13) and virtual machines (BrnoVMRS06). These runners are long-lived and persistent: identical hostnames and serial device nodes (/dev/serial_ports/*) were observed serving multiple workflow runs across days and weeks. Because these machines are non-ephemeral, any state alteration or planted binary persists across workflow runs.

Vulnerable workflow trigger and runner configuration (.github/workflows/hil.yml)
yaml
on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

jobs:
  run-tests:
    if: github.repository_owner == 'esp-rs' # Ineffective guard
    runs-on: [self-hosted, linux, ARM64, "${{ matrix.target.runner }}"]

02.The Flawed Fork Guard: github.repository_owner Antipattern

The workflow attempted to restrict execution using if: github.repository_owner == 'esp-rs'. However, in GitHub Actions pull_request triggers, github.repository_owner consistently evaluates to the owner of the target base repository (esp-rs)—never the owner of the head fork. Consequently, this check evaluated to true for every fork pull request, offering zero protection against untrusted external contributors.

03.Execution Without Approval Gating

Public GitHub Actions run logs confirmed that the repository had not enabled the mandatory approval policy for fork pull requests ('Require approval for all outside collaborators'). An audit of the 100 most recent workflow runs revealed that 82 originated from external fork heads, and all 82 ran immediately to completion (47 success, 26 failure, 9 cancelled) with zero runs held in 'action_required'. This meant any GitHub user could fork the repository, modify the workflow file, submit a pull request, and achieve immediate arbitrary execution on Espressif's self-hosted runners.

04.Persistence Vectors & Supply Chain Blast Radius

Because the runners operate with access to host devices (dialout, gpio, i2c, spi, and docker groups) and persistent file systems, an adversary could execute an equivalent of the known TensorFlow self-hosted runner attack vector: registering a secondary background runner (RUNNER_TRACKING_ID=0 && nohup ./run.sh &), harvesting credentials (~/.runner, SSH agent keys), flashing compromised firmware onto attached test chips, or stealing repository tokens minted for subsequent base-branch runs.

Reproduction & Proof of Concept (PoC)

The vulnerability was validated end-to-end against the live repository using a neutral, non-destructive provenance marker (PR #1070, workflow run 34771920489):

  1. Phase A (Trigger Precondition Verification): Queried GitHub Actions REST API to verify that external fork pull requests were executing on self-hosted runners without maintainer approval gates.
  2. Phase B (PoC Workflow Formulation): Forked esp-rs/espflash and added .github/workflows/poc.yml containing a benign diagnostic step printing hostname, whoami, id, uname, and runner environment variables.
  3. Phase C (Execution Trigger): Created pull request #1070 from fork branch ghsa-poc against base branch main. The workflow triggered automatically without maintainer interaction.
  4. Phase D (Host Execution Verification): Monitored run 34771920489; verified successful execution on self-hosted runner 'CI17045-RPI' running Linux 6.12 aarch64 under user 'pi' with dialout, gpio, i2c, spi, and docker group privileges.
  5. Phase E (Blast Radius Modeling): Documented persistence channels (secondary runner registration, token exfiltration from subsequent runs) while strictly avoiding destructive testing.
Non-destructive proof of concept workflow (.github/workflows/poc.yml)
yaml
name: PoC - RCE on self-hosted runner

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  prove-rce:
    if: github.repository_owner == 'esp-rs' # Bypassed on fork PRs
    name: Prove arbitrary code execution on self-hosted HIL runner
    runs-on: [self-hosted, linux, ARM64]
    steps:
      - name: marker
        run: |
          echo "=== PROVENANCE VERIFICATION ==="
          hostname
          whoami
          id
          uname -a
          echo "RUNNER_NAME=$RUNNER_NAME"
          echo "RUNNER_ARCH=$RUNNER_ARCH"
          echo "RUNNER_OS=$RUNNER_OS"

Impact & Blast Radius

Critical Severity (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H — 10.0 / 10). Remote unauthenticated attackers could execute arbitrary commands directly on Espressif's self-hosted runners. Attackers gained access to attached ESP32 SoC microcontrollers via serial devices (/dev/serial_ports/*), GPIO/SPI/I2C buses, and local network segments. Furthermore, planted persistence on non-ephemeral runners exposed subsequently executed release workflows (ci.yml, changelog.yml), creating an open-source supply chain tampering vector for espflash (2,200+ stars).

Remediation & Mitigation

1) Enforce repository and organization-level fork PR approval policies ('Require approval for all outside collaborators'). 2) Decouple automated fork PR triggers from self-hosted runners by gating workflow runs behind trusted maintainer slash commands (/hil quick, /hil full, /trust). 3) Use robust repository head assertions (github.event.pull_request.head.repo.full_name == 'esp-rs/espflash') rather than base owner checks. 4) Migrate self-hosted runners toward ephemeral containerized architectures and enforce minimum GITHUB_TOKEN permissions.
Espressif maintainers addressed the finding in Pull Request #1074 ('Add slash commands for HIL testing'), replacing direct pull_request triggers on self-hosted runners with maintainer-controlled slash commands (/hil quick, /hil full, /trust user) and configuring GitHub branch approval options.
Slash command controls introduced in esp-rs/espflash PR #1074
markdown
Adds slash commands for selectively running HIL tests:

/hil quick
/hil full
/hil sdm
/hil [chip2...]
/trust user
/revoke user

Full HIL remains required in the merge queue.

References & Documentation

#Self-Hosted Runners#GitHub Actions#CI/CD Security#RCE#Supply Chain#CWE-284#CWE-306#Espressif#Disclosure