Back to Blog
high SEVERITY8 min read

How curl-pipe-shell happens in GitHub Actions and how to fix it

A GitHub Actions workflow in `action.yml` was found to pipe the output of `curl` or `wget` directly into a shell interpreter — the classic "curl | bash" install pattern. If the remote server hosting the script is compromised or the URL is hijacked via DNS or CDN attack, an attacker gains arbitrary code execution inside the CI runner with full access to secrets and build artifacts. The fix replaces the unsafe inline execution pattern with a download-verify-then-execute approach.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

The "curl pipe shell" vulnerability (CWE-494, Download of Code Without Integrity Check) occurs when a GitHub Actions `run:` step passes the output of `curl` or `wget` directly to `bash` or `sh` without first verifying the downloaded content. In `action.yml`, this pattern means any compromise of the remote URL — through DNS hijacking, CDN takeover, or server breach — allows an attacker to execute arbitrary commands inside the CI runner. The fix downloads the file to disk first, verifies its cryptographic checksum or signature, and only then executes it, breaking the direct trust chain between the remote server and the shell.

Vulnerability at a Glance

cweCWE-494
fixDownload script to disk, verify checksum/signature, then execute separately
riskArbitrary code execution in CI runner; exposure of secrets and build artifacts
languageYAML (GitHub Actions)
root cause`run:` step pipes `curl`/`wget` output directly to `bash` without integrity verification
vulnerabilitycurl-pipe-shell (Download of Code Without Integrity Check)

How curl | bash Happens in GitHub Actions and How to Fix It

The Vulnerability at a Glance

Field Detail
Vulnerability curl-pipe-shell (Download of Code Without Integrity Check)
CWE CWE-494
Language YAML (GitHub Actions)
Risk Arbitrary code execution in CI runner
Root Cause run: step pipes curl/wget output directly to bash
Fix Download → verify checksum → execute separately

Introduction

The action.yml file defines a reusable GitHub Actions action — it controls exactly what commands run inside every CI job that invokes it. A security scan of this file revealed a run: step following the "curl pipe bash" install pattern: fetching a remote script and piping it directly into a shell interpreter in a single command. This is one of the highest-risk patterns in CI/CD infrastructure because it collapses the entire trust chain between a remote server and privileged code execution into a single, unverifiable step.

The pattern looks harmless in everyday developer usage — it is how many popular tools advertise their installers. But in a CI runner that holds repository secrets, deployment credentials, and signing keys, it is a loaded gun pointed at your supply chain.


The Vulnerability Explained

What the Vulnerable Pattern Looks Like

The problematic construct in action.yml follows this shape:

# VULNERABLE — do not use
- name: Install tool
  run: curl -sSfL https://example.com/install.sh | bash

or equivalently with wget:

# VULNERABLE — do not use
- name: Install tool
  run: wget -qO- https://example.com/install.sh | sh

The critical detail is the pipe (|). The shell never writes the downloaded content to disk. It reads bytes from curl's stdout and executes them immediately. There is no moment where you can inspect, hash, or verify what is being run.

Why the GitHub Actions Context Makes This Worse

A standard developer workstation running curl | bash risks only that one machine. A GitHub Actions runner is a different threat surface entirely:

  • Secrets are mounted as environment variablesGITHUB_TOKEN, cloud provider credentials, NPM tokens, and Docker Hub passwords are all accessible to any command that runs in the job.
  • The runner has network access — it can exfiltrate data, pivot to internal services, or push malicious artifacts to registries.
  • Build artifacts are trusted — code produced by CI is often signed, published, or deployed automatically. Injecting malicious code at build time poisons every downstream consumer.

The Attack Scenario

Consider a concrete attack against this specific action.yml:

  1. URL hijacking: The domain hosting install.sh expires or its DNS is hijacked. The attacker registers the domain and serves a modified script.
  2. CDN compromise: The CDN delivering the script is breached. The attacker swaps the script content at the edge node closest to GitHub's runner infrastructure.
  3. Subdomain takeover: The install URL points to a subdomain backed by a cloud resource (S3 bucket, Azure Blob, GitHub Pages) that has been deleted. The attacker claims the resource and serves arbitrary content.

In all three cases, the next time a workflow runs and reaches the curl | bash step, the attacker's code executes with full access to the runner environment — including every secret configured in the repository.

Why HTTPS Does Not Help Here

A common misconception is that https:// URLs are safe because the transport is encrypted. HTTPS verifies that you are talking to the server that owns the certificate for that domain. It does not verify that the server is serving the content you intended to download. A compromised origin server with a valid certificate is indistinguishable from a legitimate one over HTTPS.


The Fix

The correct remediation breaks the "download and execute" single step into three distinct, auditable steps:

Step 1 — Download to a Named File

- name: Download installer
  run: curl -sSfL https://example.com/install.sh -o /tmp/install.sh

Writing to a file gives you a concrete artifact you can inspect and hash.

Step 2 — Verify the Checksum

- name: Verify installer checksum
  run: |
    echo "a3f1c2d4e5b6789012345678901234567890abcdef1234567890abcdef123456  /tmp/install.sh" \
      | sha256sum --check --strict

The expected SHA-256 hash must be hardcoded in the workflow file (or fetched from a separate, pinned source). It cannot come from the same server as the script — that would be like asking the suspect to verify their own alibi.

For tools that provide GPG-signed releases, prefer signature verification over checksums:

- name: Verify GPG signature
  run: gpg --verify /tmp/install.sh.sig /tmp/install.sh

Step 3 — Execute the Verified File

- name: Run installer
  run: bash /tmp/install.sh

Only after the hash or signature matches does execution proceed.

Full Before/After Comparison

Before (vulnerable):

steps:
  - name: Install Rust toolchain helper
    run: curl -sSfL https://sh.rustup.rs | sh -s -- -y

After (safe):

steps:
  - name: Download rustup installer
    run: curl -sSfL https://sh.rustup.rs -o /tmp/rustup-init.sh

  - name: Verify rustup installer checksum
    run: |
      # Pin the expected SHA-256 from https://static.rust-lang.org/rustup/dist/
      echo "EXPECTED_HASH  /tmp/rustup-init.sh" | sha256sum --check --strict

  - name: Install Rust toolchain
    run: bash /tmp/rustup-init.sh -y

The three-step pattern eliminates the direct pipe between the network and the shell. Even if the remote server is compromised, the checksum step will fail and the workflow will halt before any malicious code executes.


Connection to the rustls-webpki Dependency Update

This PR also upgrades rustls-webpki from 0.103.9 to 0.103.13 in src-tauri/Cargo.lock to address GHSA-82j2-j2ch-gfr8 (denial of service via panic on malformed CRL BIT STRING). The Cargo.lock diff shows the version bump and the updated checksum:

 [[package]]
 name = "rustls-webpki"
-version = "0.103.9"
+version = "0.103.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"

The checksum field in Cargo.lock is exactly the kind of integrity verification that the curl | bash pattern in action.yml was missing. Cargo enforces that the downloaded crate bytes match the pinned hash before any code from that crate is compiled — the same principle that the fixed action.yml now applies to downloaded shell scripts.


Prevention & Best Practices

1. Never Pipe Network Output to a Shell

Treat curl | bash, wget | sh, curl | python, and similar patterns as unconditionally forbidden in CI. Add a Semgrep rule or actionlint to your CI pipeline to catch regressions automatically.

2. Pin Checksums in the Workflow File

The expected hash must live in version control alongside the workflow. When you upgrade the tool version, update the hash at the same time in the same commit — this creates an auditable record of intentional upgrades.

3. Prefer Official GitHub Actions Over Shell Installers

Many tools now publish official GitHub Actions (e.g., actions-rs/toolchain for Rust, dtolnay/rust-toolchain). These actions are versioned, pinned by commit SHA, and reviewed by the community. They eliminate the need for shell-based installers entirely.

# Prefer this for Rust toolchain installation
- uses: dtolnay/rust-toolchain@stable

Pin actions by commit SHA rather than tag to prevent tag mutation attacks:

- uses: dtolnay/rust-toolchain@a54c7afa936d91e3b3d8f7b8d5d0b8b0e1234567

4. Use actionlint in Your CI Pipeline

actionlint is a static analysis tool specifically for GitHub Actions workflows. It catches a range of issues including dangerous shell patterns, expression injection, and misconfigured permissions.

5. Apply Least-Privilege Permissions

Even if a curl | bash attack succeeds, limiting the workflow's permissions reduces blast radius:

permissions:
  contents: read  # restrict to minimum required

Relevant Standards

  • CWE-494: Download of Code Without Integrity Check
  • OWASP A08:2021: Software and Data Integrity Failures
  • SLSA Supply Chain Threats: Build-time dependency substitution

Key Takeaways

  • The run: step in action.yml is a privileged execution context — any code that runs there has access to all repository secrets and can modify build artifacts.
  • curl | bash eliminates every integrity check — there is no point at which you can verify that the downloaded script matches what you intended to execute.
  • HTTPS is not a substitute for checksum verification — transport security and content integrity are orthogonal properties.
  • The Cargo.lock checksum field in the same PR demonstrates the correct model — pin the expected hash of every external artifact, whether it is a Rust crate or a shell installer.
  • Official versioned Actions (pinned by commit SHA) are the safest alternative to shell-based installers for common toolchain setup tasks in GitHub Actions.

How Orbis AppSec Detected This

  • Source: Remote URL passed to curl or wget in a run: step inside action.yml
  • Sink: Shell interpreter (bash / sh) receiving the piped output of the HTTP fetch — the curl <url> | bash construct in the workflow's run: field
  • Missing control: No intermediate file write, no checksum comparison, no GPG signature verification between the network fetch and shell execution
  • CWE: CWE-494 — Download of Code Without Integrity Check
  • Fix: Replace the single-step pipe with a three-step download-verify-execute sequence, pinning the expected SHA-256 hash of the installer script in the workflow file

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The curl | bash pattern is one of the most widely deployed anti-patterns in CI/CD infrastructure, normalized by years of tool documentation that prioritizes installation convenience over security. In a GitHub Actions action.yml, it is especially dangerous because the runner environment is loaded with credentials and produces trusted artifacts. The fix is straightforward: download to a named file, verify the hash against a value pinned in version control, and only then execute. This three-step pattern is the same integrity model that Cargo applies to every dependency in Cargo.lock — and it is the right model for any code you fetch from the internet and run with elevated privilege.


References

Frequently Asked Questions

What is curl-pipe-shell in GitHub Actions?

It is a pattern where a `run:` step executes `curl <url> | bash` or `wget -O- <url> | sh`, meaning remote code is fetched and executed in one step without any integrity check. If the URL is compromised, the attacker's code runs immediately in your CI environment.

How do you prevent curl-pipe-shell in GitHub Actions?

Download the file to a temporary path first, verify its SHA-256 checksum or GPG signature against a known-good value pinned in your workflow, and only then execute the file. Never pipe network output directly to a shell.

What CWE is curl-pipe-shell?

CWE-494 — Download of Code Without Integrity Check. The downloaded code is executed without verifying that it matches a trusted, expected version.

Is HTTPS enough to prevent curl-pipe-shell?

No. HTTPS protects the transport layer but does not protect against a compromised origin server, a hijacked CDN, or a typosquatted domain. A checksum or cryptographic signature pinned in the workflow is required.

Can static analysis detect curl-pipe-shell?

Yes. Tools like Semgrep (rule `gha-curl-pipe-shell`), actionlint, and GitHub's own code scanning can flag `curl | bash` and `wget | sh` patterns in YAML workflow and action files automatically.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1346

Related Articles

high

How Denial of Service via Unbounded Intermediate Arrays happens in JavaScript and how to fix it

CVE-2026-69152 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package (versions prior to 1.1.18/2.1.4/3.0.6/5.0.9) that allows attackers to crash a Node.js application by crafting glob patterns that generate unbounded intermediate arrays, effectively bypassing the earlier CVE-2026-14257 mitigation. The fix upgrades `brace-expansion` from 1.1.14 to 1.1.18 in `frontend/package-lock.json`, closing the bypass and restoring safe memory bounds during pattern expansion.

high

How Quadratic CPU Consumption happens in JavaScript YAML parsing and how to fix it

A high-severity denial-of-service vulnerability (GHSA-5p4m-2wfm-xmqj) was discovered in js-yaml affecting both the 3.x and 4.x branches, where parsing YAML documents containing `!!omap` tags triggers quadratic CPU consumption. The fix upgrades js-yaml from `^4.1.1` to `5.2.0` in the project's GitHub Actions workflow dependencies, closing the attack surface for any untrusted YAML input processed by CI/CD tooling.

critical

How Missing Rate Limiting happens in Express.js and how to fix it

Two public API endpoints in `server.js` — `/api/health` and `/api/contact` — were exposed without any rate limiting middleware, allowing attackers to exhaust server resources or spam an SMTP server with unlimited requests. The fix adds rate limiting to both endpoints, with stricter controls on the resource-intensive `/api/contact` route that triggers email sending operations. This change closes a directly exploitable denial-of-service vector in a production web service.

high

How Denial of Service via Specific Input Sequence happens in JavaScript (marked) and how to fix it

CVE-2026-41680 is a high-severity Denial of Service vulnerability in the marked Markdown parsing library, affecting versions prior to 18.0.2. By supplying a crafted input sequence to the parser, an attacker can cause the application to hang or exhaust resources, making the frontend unavailable. Upgrading marked from 18.0.0 to 18.0.2 in both `package.json` and `package-lock.json` closes the vulnerability without affecting valid Markdown rendering.

high

How Quadratic CPU Consumption happens in JavaScript YAML parsing and how to fix it

A high-severity denial-of-service vulnerability in js-yaml (GHSA-5p4m-2wfm-xmqj) caused quadratic CPU consumption when resolving `!!omap` YAML types in both the 3.x and 4.x branches. The fix upgrades js-yaml from 3.14.2 to 3.15.1 and from 4.1.1 to 4.3.1, eliminating the algorithmic complexity exploit while leaving all valid YAML inputs unaffected.

high

How Denial of Service via Unbounded Data Happens in JavaScript and how to fix it

CVE-2025-58754 is a high-severity Denial of Service vulnerability in the popular axios HTTP client library, caused by the absence of a data size check on incoming response or request payloads. An attacker who can influence the size of data processed by axios could exhaust server memory or CPU, bringing down dependent Node.js applications. The fix upgrades axios from version 1.8.4 to 1.18.0, closing the unbounded data processing path.