High

GitHub Actions PR Metadata Script Injection RCE

Many CI workflows insert pull-request data — like the PR title, description, branch name, or a comment — directly into a shell command. If that text isn't sanitized, an attacker can put shell code inside a PR field and get it executed on the runner with the workflow's permissions. This rule watches for the runner actually spawning a process built from that tainted input.

How the attack works

An attacker opens a pull request or posts a comment containing shell metacharacters or an unevaluated GitHub Actions expression (${{ ... }}) instead of plain text. If the workflow interpolates that field into a `run:` step, the runner executes it as part of a shell command. Detection looks for a spawned process whose command line still contains a literal `${{ }}` expression (meaning attacker text carrying Actions template syntax survived into the script), or that exfiltrates CI credentials ($GITHUB_TOKEN, the environment, the event payload) to a network endpoint, or that appends loader-hijack variables (like LD_PRELOAD, NODE_OPTIONS) to $GITHUB_ENV/$GITHUB_PATH to contaminate later steps. Generic download-and-execute or reverse-shell commands are only flagged when they also carry a clear runner-context marker (paths or env vars specific to GitHub Actions runners), so the rule doesn't fire on ordinary developer machines. A separate, most-direct pattern looks for a shell breakout character together with an explicit reference to pull-request metadata in the same command.

Netzilo detection

Netzilo reports this behaviour when it is observed.

Signature ID
7f2c9a41-3d5e-4b18-9c6a-0e83b7d41af2
Severity
High

Why it matters

Successful exploitation runs attacker-controlled code inside the CI runner with the workflow's privileges, which typically means access to repository secrets, the ability to push code or modify the repository, or a foothold for escalating into later pipeline steps.

What you can do

  • Never interpolate pull_request title, body, head_ref, or comment fields directly into a `run:` shell step; pass them through an environment variable instead so they aren't parsed as shell syntax.
  • Restrict GITHUB_TOKEN permissions to the minimum needed per workflow and avoid granting write access to workflows that run on pull_request_target.
  • Review workflows for any step that writes untrusted PR data into $GITHUB_ENV or $GITHUB_PATH, since that lets injected values affect subsequent steps.
  • Audit CI logs for commands containing literal ${{ }} sequences or unexpected loader-hijack environment variables, which indicate injected text survived into execution.

Known benign look-alikes

  • Workflow linters and CI security scanners (actionlint, zizmor, semgrep, gitleaks, checkov) whose argv carries literal "${{ ... }}" expression samples or injection signatures -- suppressed by filter_security_tooling.
  • Repositories that generate, template or rewrite their own .github/workflows/*.yml with yq, envsubst, sed, awk or grep -- suppressed by filter_workflow_authoring (tool name AND workflow path must both appear).
  • Test suites that execute recorded script-injection fixtures from testdata/, fixtures/ or golden/ directories -- suppressed by filter_test_fixture.
  • Self-hosted or custom runner bootstrap steps that legitimately pipe a toolchain installer to a shell while also referencing $RUNNER_TEMP or /home/runner/work in the same command line (e.g.

References