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
- https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#understanding-the-risk-of-script-injections
- https://securitylab.github.com/resources/github-actions-untrusted-input/
- https://owasp.org/Top10/A03_2021-Injection/
- https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/