Vulnerabilities in pkg:golang/github.com/modelcontextprotocol/registry
PACKAGECVE-2026-45781 is a LOW (CVSS 3.5) oci validator skips ownership check vulnerability (CWE-636: Not Failing Securely ('Failing Open')) affecting MCP Registry libraries in the Go ecosystem. The issue involves oci validator skips ownership check on upstream rate limits. Remediation is available by upgrading affected packages to version 1.7.9.
pkg:golang/github.com/modelcontextprotocol/registry
Vulnerability advisories affecting package pkg:golang/github.com/modelcontextprotocol/registry. Select an advisory below to view technical details or fixes.
| Advisory ID | CVE / NVD | Severity | Summary |
|---|---|---|---|
| GHSA-2v5f-5r6w-p67r | CVE-2026-45781 | LOW | MCP Registry: OCI validator skips ownership check on upstream rate limits |
| GHSA-95c3-6vvw-4mrq | CVE-2026-44428 | LOW | MCP Registry's GitHub OIDC tokens are replayable across registry deployments due to shared audience |
| GHSA-v8vw-gw5j-w7m6 | CVE-2026-44427 | MODERATE | MCP Registry has open redirect via protocol-relative path in trailing-slash middleware |
| GHSA-r48c-v28r-pf6v | CVE-2026-44430 | MODERATE | MCP Registry has an unauthenticated SSRF: HTTP namespace verification dials 6to4 / NAT64 / site-local IPv6 addresses, bypassing private-address allowlist |
| GHSA-rqv2-m695-f8j4 | CVE-2026-44429 | MODERATE | MCP Registry vulnerable to stored XSS in catalogue UI via attribute-quote breakout in publisher-controlled `websiteUrl` |
CVE-2026-45781 at a Glance
Security Impact
CVSS: 3.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N).
How to Fix CVE-2026-45781
To remediate this vulnerability, upgrade all affected MCP Registry dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/modelcontextprotocol/registry to 1.7.9
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/modelcontextprotocol/registry
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-45781, inspect your project dependencies for MCP Registry:
inspect dependencies for github.com/modelcontextprotocol/registry
Analyze the terminal output to locate any version of MCP Registry in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.7.9), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-636: Not Failing Securely ('Failing Open'), where when the product encounters an error condition or failure, its design requires it to fall back to a state that is less secure than other options that are available, such as selecting the weakest encryption algorithm or using the most permissive access control restrictions. In MCP Registry, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-45781 Affected MCP Registry Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/modelcontextprotocol/registry
|
Go |
< 1.7.9
|
1.7.9
|
References
Frequently Asked Questions
What is CVE-2026-45781?
Which MCP Registry versions are affected?
What version fixes CVE-2026-45781?
Is github.com/modelcontextprotocol/registry affected?
What is the CVSS score of CVE-2026-45781?
What CWE is associated with CVE-2026-45781?
How can I check Go dependencies for CVE-2026-45781?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-2v5f-5r6w-p67r",
"modified" : "2026-05-19T15:39:56Z",
"published" : "2026-05-19T15:39:55Z",
"aliases" : [ "CVE-2026-45781" ],
"summary" : "MCP Registry: OCI validator skips ownership check on upstream rate limits",
"details" : "# OCI ownership validation fails open on upstream rate limits, allowing attacker to claim arbitrary public OCI images under their own namespace\n\nSeverity: Low (re-scored post-triage; see Maintainer triage note below)\nAffected: `modelcontextprotocol/registry` main branch at commit `fe0cb3b` (current HEAD as of 2026-05-09).\nLive deployment: `https://registry.modelcontextprotocol.io` (per repo README).\nRoute: GitHub private security advisory (per repo SECURITY.md).\n\n---\n\n## Title\n\nOCI ownership validation skips label-match check when upstream OCI registry returns HTTP 429, letting any authenticated publisher bind their `io.github.<user>/*` namespace to OCI images they do not control.\n\n## Summary\n\n`internal/validators/registries/oci.go:104-119` fails open on `http.StatusTooManyRequests`: when the\nregistry's anonymous fetch to the upstream OCI registry is rate-limited, `ValidateOCI` returns `nil`\nand the publish is accepted without ever running the\n`io.modelcontextprotocol.server.name` label-match check at lines 122-141. That label check is the\nonly cross-system ownership proof the registry applies to OCI packages — every other registry type\n(NPM, PyPI, NuGet, MCPB) treats a non-200 upstream response as a hard error.\n\nThe fail-open trigger is attacker-controllable. The registry uses `authn.Anonymous` against Docker\nHub, which is rate-limited to 100 manifest pulls per 6 hours per egress IP, and the production\nNGINX rate limit allows 180 publishes/minute (3 RPS, burst 540) per source IP. A single attacker\nfrom a single IP can exhaust the registry's shared anonymous quota in roughly 33 seconds, then\nsubmit a final publish that points `packages[].identifier` at a Docker Hub image they do not own.\nThe validator hits the 429 fail-open branch, returns `nil`, and the registry stores a record under\nthe attacker's namespace claiming the unrelated image as its package payload, with no label proof\nin evidence.\n\nThe fail-open is also reached without an attacker present. Docker Hub routinely 429s busy egress IPs\nduring organic traffic, so publishes during those windows skip OCI ownership validation silently.\n\n## Vulnerable code\n\n`internal/validators/registries/oci.go:97-142`:\n\n```go\nimg, err := remote.Image(ref, remote.WithAuth(authn.Anonymous), remote.WithContext(timeoutCtx))\nif err != nil {\n if errors.Is(err, context.DeadlineExceeded) {\n return fmt.Errorf(\"OCI image validation timed out after 30 seconds for '%s'. The registry may be slow or unreachable\", pkg.Identifier)\n }\n\n var transportErr *transport.Error\n if errors.As(err, &transportErr) {\n switch transportErr.StatusCode {\n case http.StatusTooManyRequests:\n // Rate limited - skip validation to avoid blocking publishers\n // This is intentional: we prioritize UX over strict validation during high traffic\n log.Printf(\"Skipping OCI validation for %s due to rate limiting\", pkg.Identifier)\n return nil // <-- FAIL-OPEN\n case http.StatusNotFound:\n return fmt.Errorf(\"OCI image '%s' does not exist in the registry\", pkg.Identifier)\n case http.StatusUnauthorized, http.StatusForbidden:\n return fmt.Errorf(\"OCI image '%s' is private or requires authentication. Only public images are supported\", pkg.Identifier)\n }\n }\n return fmt.Errorf(\"failed to fetch OCI image: %w\", err)\n}\n\n// Get the image config which contains labels\nconfigFile, err := img.ConfigFile()\nif err != nil {\n return fmt.Errorf(\"failed to get image config: %w\", err)\n}\n\n// Validate the MCP server name label\nif configFile.Config.Labels == nil {\n return fmt.Errorf(\"OCI image '%s' is missing required annotation. Add this to your Dockerfile: LABEL io.modelcontextprotocol.server.name=\\\"%s\\\"\", pkg.Identifier, serverName)\n}\n\nmcpName, exists := configFile.Config.Labels[\"io.modelcontextprotocol.server.name\"]\nif !exists {\n return fmt.Errorf(\"OCI image '%s' is missing required annotation. Add this to your Dockerfile: LABEL io.modelcontextprotocol.server.name=\\\"%s\\\"\", pkg.Identifier, serverName)\n}\n\nif mcpName != serverName {\n return fmt.Errorf(\"OCI image ownership validation failed. Expected annotation 'io.modelcontextprotocol.server.name' = '%s', got '%s'\", serverName, mcpName)\n}\n```\n\nThe fail-open returns before any of the three label-match guards run.\n\nThe validator is reached on every publish per `internal/service/registry_service.go:151-158`, gated by\n`cfg.EnableRegistryValidation`, which defaults to `true` in `internal/config/config.go:18`.\n\n## Reachability and authorization\n\n`POST /v0/publish` (and `/v0.1/publish`) is registered with bearer-JWT auth in\n`internal/api/handlers/v0/publish.go:30-50`. JWTs are issued by `/v0/auth/github-at`\n(`internal/api/handlers/v0/auth/github_at.go:46-67`), which exchanges any GitHub OAuth access token for\na 5-minute registry JWT carrying `Permission{Action: Publish, ResourcePattern: \"io.github.<login>/*\"}`.\nAny free GitHub account can mint such a JWT, so the publish path is reachable to anyone on the\ninternet at the cost of a GitHub account.\n\n## Trigger conditions\n\n- `internal/validators/registries/oci.go:97`: anonymous Docker Hub auth, subject to the 100\n manifest-pulls/6h/IP unauthenticated rate limit Docker Hub publishes.\n- `deploy/pkg/k8s/registry.go:330-331`: production NGINX limits incoming requests to 180/minute\n per source IP with a 3× burst multiplier (540).\n- A single source IP at 3 RPS exhausts the registry's anonymous Docker Hub quota in roughly 33\n seconds. Each `/publish` against an allowlisted OCI identifier in\n `internal/validators/registries/oci.go:29-42` (docker.io / registry-1.docker.io / index.docker.io\n / ghcr.io / quay.io / mcr.microsoft.com / `*.pkg.dev` / `*.azurecr.io`) consumes one slot,\n including publishes that go on to fail with the missing-annotation error after the manifest is\n fetched.\n- Once Docker Hub starts returning 429, every subsequent publish hits the fail-open branch until\n the quota replenishes.\n\n## Attacker chain\n\n1. Free GitHub account `attacker` → `POST /v0/auth/github-at` → registry JWT with\n `Permission{Action: Publish, ResourcePattern: \"io.github.attacker/*\"}`.\n2. From a single IP, send ~100 publishes whose `packages[].identifier` references real public\n Docker Hub images that lack the `io.modelcontextprotocol.server.name` label\n (e.g. `docker.io/library/alpine:latest`, `docker.io/library/nginx:latest`, …). Each publish\n fails with \"OCI image is missing required annotation\" but consumes one anonymous-quota slot\n from the registry's shared egress IP.\n3. While the egress IP is rate-limited by Docker Hub, submit the final publish:\n `name = \"io.github.attacker/<typo-squat-name>\"`,\n `packages[].registryType = \"oci\"`,\n `packages[].identifier = \"docker.io/<reputable-org>/<reputable-image>:<tag>\"`.\n4. `ValidateOCI` calls `remote.Image(ref, authn.Anonymous, …)`; Docker Hub returns 429;\n `transportErr.StatusCode == http.StatusTooManyRequests` matches the fail-open branch;\n `ValidateOCI` returns `nil`; `ValidatePackage` returns `nil`;\n `validateRegistryOwnership` returns `nil`; the publish proceeds and `CreateServer` writes the\n record. The registry now publishes a server record under `io.github.attacker/<typo-squat-name>`\n that asserts the reputable image as its package payload, without ever inspecting that image's\n labels.\n\n## Boundary delta\n\n| | Starting capability | After exploit |\n|---|---|---|\n| Identity | Holder of a fresh `io.github.<attacker>` GitHub account | Same |\n| Publish scope | `io.github.<attacker>/*` only | `io.github.<attacker>/*` only (unchanged) |\n| OCI claim scope | OCI images the attacker controls and has labelled with `io.modelcontextprotocol.server.name = io.github.<attacker>/<name>` | **Any public OCI image** at any allowlisted registry, regardless of label |\n\nThe attacker's namespace stays bounded. What changes is that the registry's claim \"this OCI image is\nthe package payload of this MCP server\" is no longer backed by any cross-system proof. The label\ncheck at `oci.go:122-141` is the only ownership proof for OCI packages; bypassing it lets a\npublisher under `io.github.attacker/*` bind a server record to an unrelated image such as\n`docker.io/microsoft/<some-tool>:latest` without ever touching that image. Combined with how MCP\nclients render server-list entries — image identifier shown next to the namespace — the result is\ntypo-squat / impersonation in registry search and discovery surfaces, with the actual image content\ndelivered untouched from its real owner.\n\nThe same fail-open is reached without any attacker action whenever Docker Hub rate-limits the\nregistry's egress IP for organic reasons. In that mode, the OCI ownership check is effectively\nnon-functional for the duration of the limit window, even for legitimate publishers.\n\n## Cross-validator comparison (negative control)\n\nThe other registry-type validators do not fail-open on rate-limit responses:\n\n- `internal/validators/registries/npm.go:72-74` — `if resp.StatusCode != http.StatusOK { return error }`.\n- `internal/validators/registries/pypi.go:76-78` — same shape; 429 surfaces as\n `\"PyPI package '%s' not found (status: %d)\"`.\n- `internal/validators/registries/nuget.go:253` — non-OK response paths return\n `\"NuGet README request returned status %d\"`, the publish fails closed.\n- `internal/validators/registries/mcpb.go:84-91` — a HEAD that does not return 200 or a 3xx with\n `Location` is treated as inaccessible.\n\nOCI is the only validator that converts an upstream rate-limit into a successful ownership\nattestation.\n\n## Suggested fix\n\nTwo options, either alone, or both for defence-in-depth:\n\n1. Remove the fail-open. Replace\n ```go\n case http.StatusTooManyRequests:\n log.Printf(\"Skipping OCI validation for %s due to rate limiting\", pkg.Identifier)\n return nil\n ```\n with an error of the same shape the other validators use (`return fmt.Errorf(\"OCI registry is\n currently rate-limiting validations for '%s'; please retry shortly\", pkg.Identifier)`). The\n handler call sites in `validateRegistryOwnership` already propagate the error to a 400 response.\n2. Replace `authn.Anonymous` at `internal/validators/registries/oci.go:97` with an authenticated\n token whose quota is isolated from organic anonymous traffic to the registry's egress IP. Docker\n Hub authenticated pulls are 200/6h per token; ghcr.io / quay.io / `*.pkg.dev` / `*.azurecr.io`\n each have their own auth flows. This removes the easy attacker-side trigger and reduces organic\n fail-open windows.\n\nIf a fail-open path is retained for UX reasons, queue the publish for re-validation when the\nupstream registry recovers, instead of marking it accepted on first attempt.\n\n## Proof of concept\n\nThe refreshed PoC drives the publish path, not only the validator branch:\n\n```text\nservice.CreateServer\n -> validators.ValidatePublishRequest\n -> registries.ValidateOCI\n -> database.CreateServer\n```\n\nIt runs inside the checked-out module, uses the real service and validator code, and substitutes only\nthe database with a minimal in-memory implementation so the proof can run without a local Postgres\nstack. To keep the proof localhost-only, the runner temporarily adds the in-process mock OCI host to\nthe unexported OCI allowlist. It does not contact Docker Hub, the production registry, or any\nexternal service.\n\nTo run:\n\n```bash\nbash outputs/poc-evidence/2026-05-12-mcp-registry-publish-path/run.sh\n```\n\nCaptured transcript:\n\n```text\n=== modelcontextprotocol/registry publish-path OCI 429 fail-open PoC ===\nPath exercised: service.CreateServer -> validators.ValidatePublishRequest -> registries.ValidateOCI -> DB CreateServer\n\n--- negative control: upstream 404 ---\n[setup] temporarily allowlisted mock OCI host 127.0.0.1:39067 for localhost-only proof\n[setup] publish identifier=127.0.0.1:39067/reputable-org/reputable-image:latest\n[mock-oci] GET /v2/ -> 404\n[publish] rejected: registry validation failed for package 0 (127.0.0.1:39067/reputable-org/reputable-image:latest): OCI image '127.0.0.1:39067/reputable-org/reputable-image:latest' does not exist in the registry\n\n--- BUG: upstream 429 ---\n[setup] temporarily allowlisted mock OCI host 127.0.0.1:40487 for localhost-only proof\n[setup] publish identifier=127.0.0.1:40487/reputable-org/reputable-image:latest\n[mock-oci] GET /v2/ -> 429\n[memdb] AcquirePublishLock(io.github.attacker/typosquat-tool)\n[memdb] CreateServer stored name=io.github.attacker/typosquat-tool version=1.0.1 package=127.0.0.1:40487/reputable-org/reputable-image:latest\n[publish] accepted/stored packages=[{\"registryType\":\"oci\",\"identifier\":\"127.0.0.1:40487/reputable-org/reputable-image:latest\",\"transport\":{\"type\":\"stdio\"}}]\nPUBLISH_PATH_RESULT: ACCEPTED_UNVERIFIED_OCI_PACKAGE_AFTER_429\n```\n\nExit code 0. SHA-256 values:\n\n```text\nacf7121111c19acaca1c99a3c08079213794ffc4feb63e545ec814bd6cd85984 transcript.txt\n340e7a81740e9f14cadc144d4e640a1d497ce3e6696a3d9ea99d63e05c5edd71 publish_path_runner.go\nc970f08d6b79852308ad931da85dd64a65fe373d3c988018de09a7e4c7c345a4 run.sh\n```\n\nThe end-to-end attacker flow against production was not executed. No publish was sent against\n`registry.modelcontextprotocol.io`. No attacker namespace was registered on the live service. The\nlocal proof shows the critical property: when the actual publish validator sees an OCI 429, the\nservice proceeds to create a server record containing the unverified OCI package identifier.\n\n## Severity rationale\n\n**Maintainer triage (2026-05-13):** after review the maintainer settled on Low (3.5, `CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N`). Impact stays within the attacker's own namespace and image bytes delivered to clients are unchanged. See the comment thread for reasoning. Reporter's original write-up preserved below.\n\nMedium. Auth-bypass class — the attacker bypasses the only ownership proof for OCI packages, and the\nfail-open trigger is attacker-controllable from a single IP at modest cost. The blast radius is\nbounded to publication misrepresentation under the attacker's own namespace; the actual image\ncontent stays under its rightful owner. Combined with normal MCP-client search and discovery\nsurfaces, this is sufficient for impersonation / typo-squat where the rendered image identifier\nimplies authorship the registry could not actually attest.\n\nThe fail-open also activates under normal traffic when Docker Hub rate-limits the egress IP, so the\nOCI ownership check is in practice intermittent rather than absent — both modes are bug states.\n\n## Disclosure preferences\n\nReport through the GitHub Security Advisory process per repo SECURITY.md. Happy to keep details\nprivate until a fix is in motion. If a public GHSA / CVE / release note is published, please credit\nthe report to **Ryan Vonbrubeck / @dodge1218**.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/modelcontextprotocol/registry"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.7.9"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/modelcontextprotocol/registry/security/advisories/GHSA-2v5f-5r6w-p67r"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-45781"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/modelcontextprotocol/registry"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-636" ],
"severity" : "LOW",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-19T15:39:55Z",
"nvd_published_at" : "2026-05-14T21:16:48Z"
}
}