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).
Docker Model Runner (docker/model-runner). Shipped in Docker Desktop; also the OCI client behind `docker model pull` and the Model Runner HTTP API.
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).
High. Unauthenticated blind SSRF to internal services including cloud IMDS credential endpoints. Same reachability band as the parent CVE-2026-33990.
`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).
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.
`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.
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.
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.
Proof-of-concept (private, unlisted gist): gist.github.com/babakizo420/10d93fcf750345265a
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). The gist above contains the mock servers (`mockservers.go`), the runner (`run_e2e2.sh`, which self-generates a throwaway localhost cert), and a captured evidence 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
```
Reporter: Kingsley Olukanni (GitHub: babakizo420)
Contact: kingsley@securva.net
Thank you,
Kingsley Olukanni