GitHub Actions workflows can access various sources of untrusted data through the GitHub context, such as:
When this untrusted data is directly interpolated into shell commands using GitHub’s expression syntax (${{ … }}), it can lead to
script injection vulnerabilities. An attacker who can control the content of these data sources (for example, by creating a pull request with a
malicious title) can potentially execute arbitrary commands on the runner.
It’s important to understand that GitHub Actions expressions (${{ … }}) are expanded before the shell script is
executed. This expansion happens at the workflow processing stage, not within the shell itself. This means:
Even seemingly safe assignments like pr_title="${{ github.event.pull_request.title }}" are vulnerable because the expansion happens
first. For example, if an attacker creates a pull request with a title containing "; rm -rf / #, the workflow will expand the expression
to:
pr_title=""; rm -rf / #"
The shell will then interpret this as:
pr_title="" (assigns empty string) ; rm -rf / # (hides the remaining quote) This demonstrates that any use of untrusted data in GitHub expressions within a run block is dangerous, regardless of
whether it appears to be a simple assignment or a direct command.
The consequences of successful script injection attacks in GitHub Actions can be severe and far-reaching:
Attackers can extract sensitive information from the runner environment, including:
Successful command injection allows attackers to:
Compromised workflows can be used to:
Attackers may be able to:
The impact is particularly severe because GitHub Actions runners often have elevated privileges and access to sensitive resources, making them attractive targets for attackers.
To prevent command injection in GitHub Actions workflows, avoid directly interpolating untrusted data into shell commands. Instead, use environment variables to safely pass untrusted data without risk of code execution.
The recommended approach is to assign untrusted data to environment variables using the env key, then reference these variables using
standard shell variable syntax ($VARIABLE_NAME or ${VARIABLE_NAME}) rather than GitHub’s expression syntax (${{ …
}}).
The following GitHub Action is vulnerable to command injections as it uses untrusted input directly in a run command:
name: Example
on:
pull_request:
branches: [ main ]
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Example Step
run: |
pr_title="${{ github.event.pull_request.title }}" # Noncompliant: expansion happens before shell execution
echo "PR title: $pr_title"
If an attacker creates a pull request with a title containing "; rm -rf / #, the expression expansion will transform the assignment
into pr_title=""; rm -rf / #", which the shell will execute as multiple commands, not just an assignment.
Workflow inputs of type string (which is the default type) are also vulnerable when expanded directly in shell commands:
name: Example
on:
workflow_dispatch:
inputs:
package_name:
description: 'Package name to install'
required: true
type: string
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Install package
run: |
apt-get install ${{ inputs.package_name }} # Noncompliant: string input expanded directly
Use environment variables to safely pass untrusted data:
name: Example
on:
pull_request:
branches: [ main ]
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Example Step
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "PR title: $PR_TITLE"
To securely use workflow inputs, assign them to environment variables just like any other untrusted data. Alternatively, change the input type from
string to choice with predefined options. This restricts user input to safe values controlled by the workflow author:
name: Example
on:
workflow_dispatch:
inputs:
package_name:
description: 'Package name to install'
required: true
type: choice
options:
- nginx
- apache2
- postgresql
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Install package
run: |
apt-get install ${{ inputs.package_name }}
The key difference lies in when and how the untrusted data is processed:
${{ github.event.pull_request.title }} directly in a shell command,
GitHub Actions processes the workflow file and expands the expression before the shell script runs. The expansion happens at the
workflow processing stage:
${{ github.event.pull_request.title }} and replaces it with the actual PR title value env key, GitHub Actions still expands the expression, but the expansion happens in a different context:
${{ github.event.pull_request.title }} and assigns the value to an environment variable $PR_TITLE) When the shell evaluates $PR_TITLE, it performs variable expansion (not expression expansion), which treats the value as a string
literal. Even if the value contains shell metacharacters, they remain as literal characters and are not interpreted as commands. This separation
ensures that malicious shell syntax in the untrusted data cannot be interpreted as commands, effectively neutralizing injection attempts.
string inputs, choice inputs restrict user input
to a predefined list of options that are controlled by the workflow author. When a choice input is used directly in a GitHub expression
within a run block, it is safe because:
options list It is important to note that fixing this vulnerability requires updating the workflow files in all existing branches of the repository. GitHub Actions workflows can be triggered from any branch, and an attacker could potentially target vulnerable workflows in older or feature branches. A comprehensive fix should include updating all branches that contain the vulnerable workflow patterns.
While storing untrusted data in environment variables is the correct approach, it is important to reference them properly. Simply putting the data
in an environment variable but then accessing it through GitHub’s expression syntax (${{ env.VARIABLE_NAME }}) will still result in
command injection vulnerabilities.
The following example demonstrates this common mistake:
name: Example
on:
pull_request:
branches: [ main ]
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Example
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "PR title: ${{ env.PR_TITLE }}" # Noncompliant
Beyond fixing the immediate security issue, consider implementing additional security measures:
permissions for the GITHUB_TOKEN and avoiding overly broad access. For enhanced security, implement comprehensive input validation: