Back to Blog
high SEVERITY6 min read

How mutable action tag vulnerabilities happen in GitHub Actions and how to fix it

The `bench-probe.yml` workflow referenced a GitHub Action by a mutable tag or branch name instead of a full commit SHA, leaving the pipeline exposed to supply-chain attacks if the action owner's tag were ever repointed. The fix pins the action to an immutable 40-character commit SHA, closing the same class of gap exploited in the real-world trivy-action and kics-github-action compromises.

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

Answer Summary

This is a GitHub Actions mutable action tag vulnerability (CWE-829) in `.github/workflows/bench-probe.yml`, where a workflow step referenced an action by a tag or branch name (e.g. `@v4` or `@main`) rather than a pinned commit SHA. Because tags and branches can be silently repointed by the action's maintainer or a compromised maintainer account, this creates a supply-chain attack vector. The fix is to pin the `uses:` reference to a full 40-character commit SHA, such as `actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608`.

Vulnerability at a Glance

cweCWE-829 (Inclusion of Functionality from Untrusted Control Sphere)
fixPin the `uses:` reference to a full 40-character immutable commit SHA
riskSupply-chain compromise via a repointed tag/branch executing malicious code in CI
languageYAML (GitHub Actions workflow)
root causeWorkflow step in `bench-probe.yml` used a mutable version tag/branch instead of a commit SHA for a third-party action
vulnerabilityGitHub Actions Mutable Action Tag Reference

Introduction

The .github/workflows/bench-probe.yml file drives the automated benchmarking pipeline for this repository — the same pipeline responsible for building and running performance probes like bench/wordcount/wordcount.c. Like most CI configuration, it invokes third-party GitHub Actions using the standard uses: syntax. The problem: at least one step referenced its action by a mutable tag or branch name rather than an immutable commit SHA — a pattern flagged as high severity by Semgrep's github-actions-mutable-action-tag rule.

This might look harmless in a review — after all, uses: some-action@v4 is everywhere in GitHub Actions examples online. But that convenience is exactly what makes it dangerous. Tags and branches are just pointers; the action's maintainer (or anyone who compromises their account) can silently move that pointer to a new, malicious commit at any time. The next time your CI runs, it pulls and executes that new code — no code review, no PR, no warning.

The Vulnerability Explained

Semgrep's github-actions-mutable-action-tag rule looks for uses: steps in workflow YAML that resolve to anything other than a full 40-character commit SHA. A typical vulnerable step looks like this:

steps:
  - name: Run benchmark probe
    uses: some-org/some-action@v4

Here, @v4 is a tag, not a commit. Tags in Git are mutable by design — the repository owner can git tag -f v4 <new-commit> and push, and every workflow referencing @v4 will pick up the new code on its next run. The same applies to branch references like @main or @master.

This is not a theoretical risk. It's the exact mechanism behind two real supply-chain incidents:

  • trivy-action compromise: a widely used security-scanning action was repointed, causing downstream CI pipelines to execute unexpected code.
  • kics-github-action compromise: a similar tag-repointing attack affected another popular scanning action used across thousands of repositories.

Attack scenario for bench-probe.yml

Imagine the mutable-tagged action in bench-probe.yml is used to check out code, set up a build environment, or upload benchmark artifacts. An attacker who compromises the action maintainer's account (via phished credentials, a leaked publish token, or a malicious contributor merge) could push a new commit to the tag that:

  1. Exfiltrates repository secrets (GITHUB_TOKEN, API keys, cloud credentials) available to the workflow.
  2. Injects a backdoor into the benchmark build artifacts.
  3. Modifies the checked-out source before the wordcount.c benchmark (or any other bench target) is compiled and run.

Because CI runners often have elevated permissions — access to secrets, push rights, or deployment credentials — a single repointed tag can turn a benchmarking workflow into a foothold for a much larger compromise.

The Fix

The fix is conceptually simple but security-critical: replace the mutable tag/branch reference with the full 40-character commit SHA that tag currently points to.

Before (vulnerable pattern):

- uses: some-org/some-action@v4

After (hardened):

- uses: some-org/some-action@8ade135a41bc03ea155e62e844d188df1ea18608  # v4

Note the trailing comment — it's good practice to annotate the SHA with the human-readable version it corresponds to, so future maintainers know what they're looking at without losing the immutability guarantee.

Why does this work? A commit SHA is a cryptographic hash of the commit's content and history. It cannot be silently repointed the way a tag can — if the underlying code changes, the SHA changes with it. Pinning to a SHA means the exact bytes of code that run in your CI today are the exact bytes that will run tomorrow, until you explicitly choose to bump the pin (ideally via a reviewed pull request, which is how Dependabot and Renovate handle SHA updates safely).

This change requires no behavioral modification to the workflow — the action still does exactly what it did before. It simply removes the attacker's ability to change that behavior out from under you.

Key Takeaways

  • bench-probe.yml referenced an action by a mutable tag/branch instead of a commit SHA — an easy-to-miss but high-severity gap.
  • Mutable tags gave an attacker who compromised the action's repository a path to silently alter CI behavior, mirroring the real trivy-action and kics-github-action incidents.
  • The fix — pinning to a full 40-character SHA (e.g., actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608) — is a drop-in change with no functional impact.
  • CI/CD configuration is part of your attack surface; it should be linted and reviewed with the same rigor as application code.
  • Automated tooling (Dependabot/Renovate + Semgrep) can keep SHA pins current without reintroducing the mutable-reference risk.

How Orbis AppSec Detected This

  • Source: The uses: directive in .github/workflows/bench-probe.yml, resolved at workflow-run time from GitHub's action registry.
  • Sink: The GitHub Actions runner, which fetches and executes whatever commit the tag/branch currently points to.
  • Missing control: No pinning to an immutable commit SHA — the workflow trusted a mutable reference to determine what code executes in CI.
  • CWE: CWE-829 (Inclusion of Functionality from Untrusted Control Sphere).
  • Fix: The action reference in bench-probe.yml was pinned to its full 40-character commit SHA, eliminating the ability for the tag/branch to be silently repointed.

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

A single unpinned uses: line in bench-probe.yml was enough to expose the benchmarking pipeline to a supply-chain attack pattern that has already been used against real projects. Pinning GitHub Actions to full commit SHAs is a small, low-friction change that removes an entire class of risk — one that's easy to overlook because tag-based references are the default example in almost every piece of GitHub Actions documentation. Treat every uses: line in your workflows as a dependency that deserves the same scrutiny as a package in your package.json or requirements.txt.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #109

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.