Skip to content

CL-0004: Unpinned image tags (implicit :latest)

Severity: MEDIUM

Derivation (see severity model):

  • Baseline: B — the attacker can read the compose file, or influence what the image reference resolves to; no code execution anywhere yet
  • Precondition: Second flaw — a mutable tag only becomes an attack when something outside this file changes what it points at: a registry compromise, a hijacked publisher account, a poisoned build
  • Impact: Single container — the substituted image runs as this service. Reaching further needs a privilege this file grants elsewhere, which is scored by the rule that grants it
  • Qualifier/modifier: none
  • Derived: Second flaw × Single container = MEDIUM
  • Shipped: MEDIUM
  • Evidence: Not runtime-observable: tag mutability is a registry property, and compose-lint does not read the registry (ADR-020). The premise is the registry API contract — a tag is a mutable pointer, and docker pull resolves it at pull time

References: - OWASP Docker Security Rule #13 - CIS Docker Benchmark 5.28 — Ensure that Docker commands always make use of the latest version of their image

What it detects

Services using mutable image tags (latest, stable, edge, nightly, dev, test) or no tag at all. Build-only services (no image: key) and digest-pinned images (@sha256:...) are skipped.

The mutable-tag list is a closed enumeration of well-known aliases. Tags like main, master, release, prod, slim, or alpine are also mutable in practice but are not flagged here — distinguishing "moving target" from "stable channel" requires registry knowledge the linter does not have. CL-0019 (digest pinning) is the stronger check; treat CL-0004 as catching the obvious cases.

Why it matters

Mutable tags mean every pull can produce a different image. This breaks reproducibility, makes rollbacks impossible, and opens supply chain risk — a compromised upstream push to latest affects you on next deploy with no visibility into what changed.

Fix

# Instead of:
image: nginx:latest

# Pin to a specific version:
image: nginx:1.27-alpine

For maximum reproducibility, use digest pinning (CL-0019). Tag pinning protects against accidental version churn; digest pinning additionally protects against an attacker overwriting a tag in the registry. Cosign signature verification or registry-level immutability flags add a further layer if your registry supports them.

image: nginx:1.27-alpine@sha256:...

Find current stable versions on Docker Hub or your registry.

Images that only publish latest

Some projects (Excalidraw, for example) publish only a latest tag and no versioned tags, so "pin the tag" is not actionable. Digest-pinning still is — and this rule already treats a digest-pinned reference as valid, because the digest is the real pin and the tag is a cosmetic label:

image: excalidraw/excalidraw:latest@sha256:<digest>

Get the current digest with:

docker buildx imagetools inspect excalidraw/excalidraw:latest --format '{{.Manifest.Digest}}'

then let Renovate bump the digest on a schedule.

This does not break when the tag moves: Docker resolves by digest and ignores the tag for content, so you keep pulling the exact image you pinned and upgrades happen only when the pin changes. The only failure case is the pinned digest being deleted from the registry (rare on Docker Hub; possible on a self-hosted registry with garbage collection enabled).

ATT&CK coverage

Remediating this finding contributes to mitigating the following MITRE ATT&CK techniques (pinned to ATT&CK v18). compose-lint is a static analyser, so this is mitigation coverage — it detects nothing at runtime.

Technique Tactic
T1204.003 User Execution: Malicious Image Execution

See also

  • CL-0019 — digest pinning (stronger guarantee against tag mutation)