Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLXVI · Third-Party Actions and PluginsRisk

Mutable references and the tag-swap attack

Advanced⏱ ~23 min🧪 Lab requiredgit

What you'll learn

  • Explain why a version tag is a mutable reference, not an immutable value
  • Describe a tag-swap attack and how it differs from a release-publish attack
  • Identify the tags most commonly used and why they are dangerous
  • Recognise the audit gap a mutable tag creates even when the workflow file is unchanged

Prerequisites

Practice

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A version tag is a pointer, not a value. The reference actions/checkout@v4 is shorthand for “the commit that the tag v4 currently points at in the actions/checkout repository”. The runner resolves that pointer at job start, downloads the commit the tag points at, and runs it. The pointer can move. Yesterday, @v4 resolved to commit a1b2c3d4.... Today, @v4 might resolve to commit e5f6a7b8.... The workflow file is unchanged; the bytes that run are not. This is the tag-swap attack.

Why a tag is a pointer

Git tags are not version numbers in a package ecosystem. In npm, RubyGems, and PyPI, ^1.2.3 is a constraint the resolver evaluates against an immutable registry. In Git, a tag is a ref: a file that contains a commit SHA. The file can be updated to point at a different commit. git push --force origin v4 overwrites the remote tag. Nothing about the tag itself records that it moved.

# Inspect the tag object locally
git cat-file -p refs/tags/v4
# -> object ${COMMIT_SHA}
# -> type commit
# -> tag v4

The first command shows a tag is just a reference to a commit SHA. The bytes that ran yesterday are in the commit v4 pointed at yesterday. The bytes that run today are in the commit v4 points at today.

Anatomy of a tag-swap attack

The attack has three steps:

  1. Compromise the tag. The attacker obtains push access to the action’s repository (stolen PAT, exploited release workflow) and force-pushes the existing tag to a new commit containing the payload.
  2. Wait for runners to resolve. Consumer workflows referencing uses: victim/action@v2 continue to use @v2 unchanged. The next runner that resolves the tag downloads the attacker’s commit.
  3. Exploit the consumer’s privileges. The malicious commit runs in the consumer’s job, with the consumer’s secrets, and exfiltrates or writes as the consumer’s identity.
flowchart LR
    A["Day 1 - tag v4"] --> B["Commit a1b2c3d"]
    A --> C["Runner resolves v4"]
    C --> D["Runs commit a1b2c3d"]
    E["Day 2 - tag moved"] --> F["Commit e5f6a7b8"]
    E --> G["Runner resolves v4"]
    G --> H["Runs commit e5f6a7b8"]
    B -. "different bytes" .-> F

The attack’s signature is its invisibility. The consumer’s repository shows no change. The workflow file shows no change. The only change is a remote tag that was force-updated by a push event the consumer does not subscribe to.

Which tags are dangerous

Every tag that is not pinned to a SHA is dangerous in principle. The attack is most credible against tags the maintainer routinely moves: major-version tags (@v1, @v2, @v3), floating tags (@latest, @main, @master), and maintainer aliases (@stable, @release). The least dangerous tags are immutable release tags the maintainer has explicitly committed never to move — and even those are at risk if the maintainer’s account is compromised.

Production discipline

  1. Never reference a third-party action by tag. Pin to a full 40-character commit SHA (covered in LXVI-05).
  2. Treat @main and @latest as code-review blockers.
  3. Re-pin on every dependency upgrade. A new SHA is a deliberate change with a deliberate review.
  4. Subscribe to the action’s security advisories.
  5. Record the resolved SHA in the workflow run log. GitHub does this automatically; export to your audit storage.

Cross-course references

  • Git, CI/CD & GitOps — Part LXVI-01 (Why third-party actions are a risk) establishes the runner-execution model this lesson exploits.
  • Git, CI/CD & GitOps — Part III (Object Model) covers Git tags as objects; the pointer semantics here are the same.
  • Git, CI/CD & GitOps — Part LXV-05 (Artifact Trust) covers the content-addressing model that makes SHA-pinning a primitive.

Quiz

Knowledge check · 4 questions

  1. Q1. What makes a tag-pinned action reference vulnerable to a tag-swap attack?

  2. Q2. A version tag is a value that the runner resolves once at workflow-write time and reuses on every run.

  3. Q3. Name the three steps of a tag-swap attack, in order.

  4. Q4. Identify the tag-swap indicator and the rule that prevents recurrence.

    Team T's workflow uses `thirdparty/deploy-helper@v3`. A security advisory was published for the action's maintainer account: a phishing email led to a stolen PAT. The attacker force-pushed the `v3` tag to a commit that reads `secrets.AWS_DEPLOY_ROLE_TOKEN` and POSTs it to an external server. Today's CI run executed the new commit. CloudTrail shows an unfamiliar STS `AssumeRole` call from a GitHub Actions IP range, six minutes after the workflow started.

Passing score: 75%. Answers are checked in this browser.