Git, CI/CD & GitOpsLXVI · Third-Party Actions and PluginsRisk
Mutable references and the tag-swap attack
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
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:
- 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.
- Wait for runners to resolve. Consumer workflows
referencing
uses: victim/action@v2continue to use@v2unchanged. The next runner that resolves the tag downloads the attacker’s commit. - 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
- Never reference a third-party action by tag. Pin to a full 40-character commit SHA (covered in LXVI-05).
- Treat
@mainand@latestas code-review blockers. - Re-pin on every dependency upgrade. A new SHA is a deliberate change with a deliberate review.
- Subscribe to the action’s security advisories.
- 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
Q1. What makes a tag-pinned action reference vulnerable to a tag-swap attack?
Q2. A version tag is a value that the runner resolves once at workflow-write time and reuses on every run.
Q3. Name the three steps of a tag-swap attack, in order.
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.