GitHub Advisoryadvisory · Go

GHSA-2v5f-5r6w-p67r MCP Registry: OCI validator skips ownership check on upstream rate limits

LOW Published 2026-05-19 Modified 2026-05-19 Official page ↗
Aliases
CVE-2026-45781
Ecosystem
Go
Packages
github.com/modelcontextprotocol/registry
Severity
Low
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N
Weaknesses
CWE-636
Reviewed
2026-05-19

Affected packages

• Go github.com/modelcontextprotocol/registry: < 1.7.9 → fixed in 1.7.9

Description

OCI ownership validation fails open on upstream rate limits, allowing attacker to claim arbitrary public OCI images under their own namespace Severity: Low (re-scored post-triage; see Maintainer triage note below) Affected: `modelcontextprotocol/registry` main branch at commit `fe0cb3b` (current HEAD as of 2026-05-09). Live deployment: `https://registry.modelcontextprotocol.io` (per repo README). Route: GitHub private security advisory (per repo SECURITY.md). --- Title OCI ownership validation skips label-match check when upstream OCI registry returns HTTP 429, letting any authenticated publisher bind their `io.github./*` namespace to OCI images they do not control. Summary `internal/validators/registries/oci.go:104-119` fails open on `http.StatusTooManyRequests`: when the registry's anonymous fetch to the upstream OCI registry is rate-limited, `ValidateOCI` returns `nil` and the publish is accepted without ever running the `io.modelcontextprotocol.server.name` label-match check at lines 122-141. That label check is the only cross-system ownership proof the registry applies to OCI packages — every other registry type (NPM, PyPI, NuGet, MCPB) treats a non-200 upstream response as a hard error. The fail-open trigger is attacker-controllable. The registry uses `authn.Anonymous` against Docker Hub, which is rate-limited to 100 manifest pulls per 6 hours per egress IP, and the production NGINX rate limit allows 180 publishes/minute (3 RPS, burst 540) per source IP. A single attacker from a single IP can exhaust the registry's shared anonymous quota in roughly 33 seconds, then submit a final publish that points `packages[].identifier` at a Docker Hub image they do not own. The validator hits the 429 fail-open branch, returns `nil`, and the registry stores a record under the attacker's namespace claiming the unrelated image as its package payload, with no label proof in evidence. The fail-open is also reached without an attacker present. Docker Hub routinely 429s busy egress IPs during organic traffic, so publishes during those windows skip OCI ownership validation silently. Vulnerable code `internal/validators/registries/oci.go:97-142`: ```go img, err := remote.Image(ref, remote.WithAuth(authn.Anonymous), remote.WithContext(timeoutCtx)) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return fmt.Errorf("OCI image validation timed out after 30 seconds for '%s'. The registry may be slow or unreachable", pkg.Identifier) } var transportErr *transport.Error if errors.As(err, &transportErr) { switch transportErr.StatusCode { case http.StatusTooManyRequests: // Rate limited - skip validation to avoid blocking publishers // This is intentional: we prioritize UX over strict validation during high traffic log.Printf("Skipping OCI validation for %s due to rate limiting", pkg.Identifier) return nil // /*"}`. Any free GitHub account can mint such a JWT, so the publish path is reachable to anyone on the internet at the cost of a GitHub account. Trigger conditions • `internal/validators/registries/oci.go:97`: anonymous Docker Hub auth, subject to the 100 manifest-pulls/6h/IP unauthenticated rate limit Docker Hub publishes. • `deploy/pkg/k8s/registry.go:330-331`: production NGINX limits incoming requests to 180/minute per source IP with a 3× burst multiplier (540). • A single source IP at 3 RPS exhausts the registry's anonymous Docker Hub quota in roughly 33 seconds. Each `/publish` against an allowlisted OCI identifier in `internal/validators/registries/oci.go:29-42` (docker.io / registry-1.docker.io / index.docker.io / ghcr.io / quay.io / mcr.microsoft.com / `*.pkg.dev` / `*.azurecr.io`) consumes one slot, including publishes that go on to fail with the missing-annotation error after the manifest is fetched. • Once Docker Hub starts returning 429, every subsequent publish hits the fail-open branch until the quota replenishes. Attacker chain 1. Free GitHub account `attacker` → `POST /v0/auth/github-at` → registry JWT with `Permission{Action: Publish, ResourcePattern: "io.github.attacker/*"}`. 2. From a single IP, send ~100 publishes whose `packages[].identifier` references real public Docker Hub images that lack the `io.modelcontextprotocol.server.name` label (e.g. `docker.io/library/alpine:latest`, `docker.io/library/nginx:latest`, …). Each publish fails with "OCI image is missing required annotation" but consumes one anonymous-quota slot from the registry's shared egress IP. 3. While the egress IP is rate-limited by Docker Hub, submit the final publish: `name = "io.github.attacker/"`, `packages[].registryType = "oci"`, `packages[].identifier = "docker.io//:"`. 4. `ValidateOCI` calls `remote.Image(ref, authn.Anonymous, …)`; Docker Hub returns 429; `transportErr.StatusCode == http.StatusTooManyRequests` matches the fail-open branch; `ValidateOCI` returns `nil`; `ValidatePackage` returns `nil`; `validateRegistryOwnership` returns `nil`; the publish proceeds and `CreateServer` writes the record. The registry now publishes a server record under `io.github.attacker/` that asserts the reputable image as its package payload, without ever inspecting that image's labels. Boundary delta | | Starting capability | After exploit | |---|---|---| | Identity | Holder of a fresh `io.github.` GitHub account | Same | | Publish scope | `io.github./*` only | `io.github./*` only (unchanged) | | OCI claim scope | OCI images the attacker controls and has labelled with `io.modelcontextprotocol.server.name = io.github./` | Any public OCI image at any allowlisted registry, regardless of label | The attacker's namespace stays bounded. What changes is that the registry's claim "this OCI image is the package payload of th…

Related

Alias · CVE (1)

  • CVE-2026-45781 The MCP Registry provides MCP clients with a list of MCP servers, like an app store for MCP servers. vulnerability

Weakness · CWE (1)

  • CWE-636 Not Failing Securely weakness

References