Security vulnerability report: Docker Model Runner: unauthenticated SSRF via unguarded manifest/blob redirect-follow (incomplete fix of CVE-2026-33990)

Hello Docker Security Team,

Reporting privately per the Docker security policy. This is a server-side request forgery (SSRF) issue in the docker/model-runner OCI client, and an incomplete fix of CVE-2026-33990. It is distinct from the Docker MCP Gateway credential-forwarding issue I reported separately (different repository, different impact, see the RELATIONSHIP note below).

PROJECT

Docker Model Runner (docker/model-runner). Shipped in Docker Desktop; also the OCI client behind `docker model pull` and the Model Runner HTTP API.

AFFECTED

Confirmed present at the latest release v1.2.6 (2026-07-09) and on `main` HEAD. The manifest/blob data-plane client in `pkg/distribution/oci/remote/remote.go` follows redirects (`rangeTransport`, up to 10 hops) with no `CheckRedirect`, and neither `isDisallowedIP` nor `resolveAndValidateRealm` is called on that path (grep count on the data-plane path = 0).

SEVERITY

High. Unauthenticated blind SSRF to internal services including cloud IMDS credential endpoints. Same reachability band as the parent CVE-2026-33990.

COMPONENT

`pkg/distribution/oci/remote/remote.go` (the manifest/blob data-plane fetch and the `rangeTransport` redirect-follow), reached via `pkg/inference/models/http_handler.go` (`POST /models/create`, CORS-only, no caller auth).

SUMMARY

The fix for CVE-2026-33990 added a real SSRF guard (`resolveAndValidateRealm` + `isDisallowedIP` covering loopback / RFC-1918 / 169.254.0.0/16 / link-local, plus a pinned dial), but wired it into the token-exchange sub-request ONLY (`Exchange`, for the `WWW-Authenticate` realm). The manifest and blob data-plane fetch, and its transport-level redirect-follow, still route through the plain unguarded transport. A crafted registry can 302-redirect a manifest/blob fetch to an internal, loopback, or cloud-metadata host, and Model Runner will issue the request verbatim with no revalidation on the redirect target. Unauthenticated entry is `POST /models/create` (CORS-only), whose `From` model reference is fully attacker-controlled.

ROOT CAUSE

`pkg/distribution/oci/remote/remote.go` (createResolver + rangeTransport, around :425 and :198):

```

// createResolver (~:425): the client that fetches manifests/blobs

transport := &rangeTransport{base: o.transport, ...} // o.transport = http.DefaultTransport (a PLAIN transport)

client := &http.Client{Transport: transport} // no CheckRedirect

// rangeTransport.RoundTrip (~:198) follows up to maxRangeRedirects (=10) hops:

for redirects := 0; redirects < maxRangeRedirects && isRedirect(resp.StatusCode); redirects++ {

location := resp.Header.Get("Location") // fully attacker-controlled, fetched verbatim

// no isDisallowedIP / resolveAndValidateRealm call on this path

}

```

So on a manifest/blob GET, the attacker registry returns `302 Location: 169.254.169.254/latest/meta-data/iam/security- (or `127.0.0.1:<port>/`), and `rangeTransport.RoundTrip` follows it with no guard.

WHY THE EXISTING GUARDS DO NOT STOP IT

RELATIONSHIP TO CVE-2026-33990 / GHSA-x2f5-332j-9xwq (IMPORTANT, NOT A DUPLICATE)

CVE-2026-33990 ("SSRF in Docker Model Runner OCI Registry Client") was fixed by guarding the token-exchange realm sub-request (`Exchange` -> `resolveAndValidateRealm` + `isDisallowedIP`). This report is a different call site the fix did not reach: the manifest/blob data-plane fetch and its `rangeTransport` redirect-follow, which never adopted the guard. The parent closed the realm path; the data-plane redirect path is the residual.

This is also distinct from the Docker MCP Gateway credential-forwarding report I filed separately (docker/mcp-gateway, `pkg/mcp/remote.go`, credential headers leaked OUT to an attacker host). That is a different repository and a different impact (credential exfiltration); this is SSRF reaching IN to internal/IMDS hosts.

IMPACT

Unauthenticated (CORS-only route) blind SSRF: the daemon issues attacker-directed GETs to loopback / RFC-1918 / 169.254.169.254 (cloud IMDS), driven by an attacker-controlled registry 302, with no revalidation on the redirect path. On a cloud host running Docker Desktop / Model Runner, IMDS credential probing is the headline impact. Minimum privilege is a local process or a container that can reach the Model Runner API (the same reachability as CVE-2026-33990).

Content exfiltration and reflection were tested and are NOT achievable: the fetched internal bytes are rejected by OCI content-addressing / config validation and are not stored (`GET /models` returns `[]`) or reflected to the caller. Severity is therefore High (blind SSRF), not Critical.

REPRODUCTION (self-hosted, no third-party infra)

Verified end-to-end against the REAL `docker/model-runner:v1.2.6` daemon, contained and loopback-only (an HTTPS mock registry trusted via `SSL_CERT_FILE`, a canary, and a mock IMDS, all on 127.0.0.1; no third-party host, no real metadata service). PoC files attached (mockservers.go, cert.pem, key.pem, run_e2e2.sh, evidence, hits.log).

Firing the unauthenticated `POST /models/create {"from":"127.0.0.1:<mock-registry>/x:latest"}`, the running binary (User-Agent `model-distribution`):

Turnkey:

```

docker pull docker/model-runner:v1.2.6

go build -o mockservers mockservers.go

./run_e2e2.sh

cat hits.log # shows the real binary reaching the internal canary and mock IMDS

```

FIX

Reporter: Kingsley Olukanni (GitHub: babakizo420)

Contact: kingsley@securva.net

Thank you,

Kingsley Olukanni

Compiled by Buddy, reviewed by your coordinator. Real + verified + cited only. Hype and out-of-window items flagged, not hidden.