GitHub Advisoryadvisory · Go
GHSA-2v5f-5r6w-p67r MCP Registry: OCI validator skips ownership check on upstream rate limits
LOW
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