Skill 189 · GitHub Actions Hardening
Subchapter 189.1
references/injection.mdMarkdown3 KBView on GitHub
${{ <expr> }} is substituted into the script as text, before the shell runs. Any expression
that resolves to data an outside contributor controls is therefore a command-injection sink.
These can be set by anyone who can open an issue, PR, or comment:
| Context | Set by |
|---|---|
github.event.issue.title / .body | Issue author |
github.event.pull_request.title / .body | PR author |
github.event.pull_request.head.ref / .head.label | PR author (branch name) |
github.head_ref | PR author (branch name) |
github.event.comment.body | Commenter |
github.event.review.body / .review_comment.body | Reviewer |
github.event.commits.*.message / head_commit.message | Commit author |
github.event.commits.*.author.email / .name | Commit author |
github.event.pages.*.page_name | Wiki editor |
A branch named $(<attacker-command>) or an issue titled "; <attacker-command> # becomes shell
when interpolated into a run: step.
# VULNERABLE
- run: |
echo "Reviewing PR: ${{ github.event.pull_request.title }}"
git checkout ${{ github.head_ref }}Bind the untrusted value to an environment variable, then reference the shell variable (quoted). The shell variable is data, never re-parsed as workflow syntax:
# SAFE
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
HEAD_REF: ${{ github.head_ref }}
run: |
echo "Reviewing PR: $PR_TITLE"
git checkout "$HEAD_REF"${{ }} now appears only on the env: side, where it is assigned as a value rather than spliced
into a command. Always quote the shell variable ("$PR_TITLE") to prevent word-splitting and
globbing.
The same rule applies. Do not interpolate ${{ }} into the script: body — pass it through the
environment and read process.env:
# VULNERABLE
- uses: actions/github-script@<sha>
with:
script: console.log("${{ github.event.issue.title }}")
# SAFE
- uses: actions/github-script@<sha>
env:
TITLE: ${{ github.event.issue.title }}
with:
script: console.log(process.env.TITLE)Passing untrusted ${{ }} into a composite or JS action’s with: inputs can be safe or not
depending on whether the action itself interpolates the input into a shell. When in doubt, pass via
env: and have the action read the environment, or sanitize/validate first (e.g. a branch name
should match ^[A-Za-z0-9._/-]+$).
run: and script: for ${{.env: with a quoted shell variable.github.actor, github.repository, github.sha, github.ref (for branch protection contexts)
and similar server-controlled values are not attacker-set, but a defense-in-depth env: rewrite
costs nothing.