Rules / GL001
GL001 Mutable image tag
Severity: error (version-less variant tags: warn)
OWASP: CICD-SEC-3 – Dependency Chain Abuse
Risk
Using latest, a branch name, or no tag at all means the image pulled by the runner can change between pipeline runs without any code change. A compromised or hijacked registry tag silently introduces malicious code into your build environment.
What is flagged
- No tag:
image: alpine(implicitly pullslatest) - Mutable tags:
latest,stable,edge,dev,main,master,nightly,rolling,canary,beta,alpha,lts,current,testing,oldstable - Version-less tags, as
warn: any tag with no digit in it, such asdocker:dind,docker:dind-rootless,node:alpine,python:slim,gcr.io/kaniko-project/executor:debugordebian:bookworm. These are thelatestbuild of a variant or release series, and upstream moves them on every release. - Applies to
image:andservices:at global,default:, and job level
SHA256 digest pins (@sha256:...) are always safe and never flagged.
Trigger example
build:
image: node:latest # mutable tag
script:
- ./build.sh
test:
image: alpine # no tag — defaults to latest
services:
- docker:dind # version-less variant tag (warn)
script:
- ./test.sh
Safe alternative
build:
image: node:20.11.0 # pinned version
script:
- ./build.sh
test:
image: alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
services:
- docker:27.3.1-dind # versioned variant
script:
- ./test.sh
Notes
A tag is treated as version-less when it contains no digit. Version-qualified variants (docker:27-dind, python:3.11-slim, node:20.11.0-alpine, ubuntu:24.04) pass through. They are reported as warn rather than error. They float just like latest, but a variant tag at least fixes the image flavour, and docker:dind in particular appears in a large share of pipelines.
A tag containing a variable ($CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA, builder:$TAG-alpine) is not flagged, since its value is unknown statically.
An image reference that is a single variable (image: $MY_IMAGE or ${MY_IMAGE}, also in services:) is resolved against the values the same file assigns to it, and each value is checked like an inline image:
- every
parallel:matrixvalue of that name in the job, each reported separately; - for jobs without such a matrix value, or matrix entries that don't set the name, the job's own
variables:; - failing that, the top-level
variables:, unlessinherit:variablesisfalseor a list that leaves the name out.
The message names both, e.g. image "$BASE_IMAGE" (= "node:latest") uses mutable tag "latest". Top-level variables are not used for hidden jobs (.template), whose users may override them, or for jobs that inherit through extends:, which glsec does not resolve. YAML merge keys (<<:) are resolved. A variable that is not defined in the file, or whose value contains another $, is not flagged, since its value is unknown statically. Variables that are never used in image: or services: (for example a docker push target) are not checked. GL007 still reports the same line, because a pipeline variable can override the value.
The VM images of GitLab-hosted macOS runners (image: macos-14-xcode-15) are selected by name, cannot carry a tag, and are not flagged.
Pinning an image prevents tag mutation but does not verify whether the pinned version contains known vulnerabilities. Use a dedicated container scanner such as trivy as a separate CI job to check for CVEs in pinned images.
Documentation for glsec 1.23.0. Source of this page · Rule implementation