Container Security 5 min read

Most CVEs in Go images aren't exploitable. How to prove which ones

baseimage Analytics Team

A big share of the most-used images on Docker Hub are Go programs: Prometheus, Grafana, Traefik, Caddy, MinIO, etcd, CoreDNS, Loki, VictoriaMetrics, Vault, Consul, ArgoCD, and nearly every exporter. If you run any of them, you've seen their scan reports: dozens of findings on a single tag.

Short version
  • • Go binaries are usually static — findings in the image's OS packages describe code the program never executes.
  • • The Go linker strips uncalled functions — govulncheck proves which module findings actually made it into the binary.
  • • Your job isn't to patch all forty findings. It's to sort them — with evidence, not opinion. Three checks do it.

What that list actually is

In a typical Go image report:

  • a large part sits in OS packages the program never runs — a Go binary is usually static and doesn't call the image's OpenSSL or libc at all;
  • another part is in library code that never made it into the binary — the Go linker strips functions that are never called;
  • some findings are in stdlib functions the app doesn't use;
  • and a few are real — those get fixed by taking a newer release.

Check 1 — Is the binary static?

Kills most OS-package findings. Pull the binary out of the image and look at it:

terminal
docker create --name tmp prom/prometheus && \
  docker cp tmp:/bin/prometheus ./prometheus && docker rm tmp
file ./prometheus   # "statically linked" → never executes the image's OS libraries

If it says statically linked — every finding against openssl, zlib, libc, busybox describes code that ships in the image but is never executed by the application. That's usually the biggest block of the report, ruled out in one command.

If it's dynamic
If it says dynamically linked, the binary does use OS libraries — treat those findings normally.

Check 2 — Did the vulnerable function make it into the binary?

Kills most module findings:

terminal
govulncheck -mode=binary ./prometheus

govulncheck is the Go team's tool. It reports a CVE only if the vulnerable function is actually present in the binary — and since the Go linker drops code that's never called, “present” is a strong signal while “absent” is a proof. In practice this rules out the large majority of module findings: function-level analysis across ecosystems shows fewer than ~10% of dependency vulnerabilities are reachable (Endor Labs).

  • Function absent → not exploitable in this binary. Dismissed, with the govulncheck output as evidence.
  • Function present → treat as real. Check go version -m ./prometheus for the module version, then take the release where it's bumped (or bump and rebuild if it's your image).
Scope
Works on binaries built with Go 1.18+. If you have the source, govulncheck ./... is even stricter — it builds the real call graph.

Check 3 — stdlib findings: check the toolchain version

Findings against stdlib live in the Go toolchain the binary was compiled with. Nothing inside the image can patch them — only a build with a newer Go.

terminal
go version -m ./prometheus | head -1   # e.g. "go1.24.2"
  • Vulnerable function not in the binary? Check 2 already cleared it.
  • It is there, and a newer Go fixes it? Take the vendor's next release — or rebuild with current Go if the image is yours.
  • Vendor stopped rebuilding? That tag is unfixable; move to a maintained one.

Write the dismissals down

For each finding you ruled out, record a short VEX statement: not_affected + justification (vulnerable_code_not_in_execute_path) + the command output as evidence. Scanners like Trivy and Grype consume VEX and stop flagging those findings on every future scan — and a security review gets proof instead of “we think it's fine”.

One rule keeps it honest
Re-run the three checks on every new build. The evidence belongs to the binary you tested, not to the image name.

The exceptions

Two cases where you fix immediately, whatever the checks say: the CVE is in CISA KEV (actively exploited) and the service faces the internet — or the vulnerable code is the app's core path.

The result

Findings blockCheckOutcome
OS packages (openssl, libc, zlib…)fileRuled out if the binary is static
Go module dependenciesgovulncheck -mode=binaryMost ruled out — function not in binary
stdlib / toolchaingo version -mSorted by toolchain version
The real fewFixed by taking a newer release

One pass on a typical Go image — and the two or three real findings get fixed by taking a newer release. Repeat on every release.

Or run Go-stack images where this triage is already done and documented: that's what BaseImage ships, re-scanned daily.

Skip the manual triage

Let an agent run it — or get it done for you

A playbook your AI agent can follow to run these checks — or a full evidence-backed report on your own image.

Harden my image

We run this triage on Go-stack images, document the evidence, and re-scan daily.