CVE-2026-44430: Github.com/modelcontextprotocol/registry HTTP Namespace Verification Dials 6to4 Vulnerability

MODERATE
Advisory Identifier: GHSA-r48c-v28r-pf6v
CVSS 4.0
CWE-918
Published
Advisory Summary

CVE-2026-44430 is a MODERATE (CVSS 4.0) http namespace verification dials 6to4 vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Github.com/modelcontextprotocol/registry libraries in the Go ecosystem. The issue involves http namespace verification dials 6to4 / nat64 / site-local ipv6 addresses, bypassing private-address allowlist. Remediation is available by upgrading affected packages to version 1.7.7.

CVE-2026-44430 at a Glance

CVE Identifier CVE-2026-44430
GHSA Identifier GHSA-r48c-v28r-pf6v
Severity MODERATE
CVSS v3.1 Base Score 4.0
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Github.com/modelcontextprotocol/registry
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 4.0

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality None (C:N)
Integrity Low (I:L)
Availability None (A:N)

How to Fix CVE-2026-44430

To remediate this vulnerability, upgrade all affected Github.com/modelcontextprotocol/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.

github.com/modelcontextprotocol/registry fixed: 1.7.7
Upgrade github.com/modelcontextprotocol/registry to 1.7.7
Verify Direct and Transitive Dependencies

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-44430, inspect your project dependencies for Github.com/modelcontextprotocol/registry:

inspect dependencies for github.com/modelcontextprotocol/registry

Analyze the terminal output to locate any version of Github.com/modelcontextprotocol/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.7), 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-918: Server-Side Request Forgery (SSRF), where the web server receives a url or similar request from an upstream component and retrieves the contents of this url, but it does not sufficiently ensure that the request is being sent to the expected destination. In Github.com/modelcontextprotocol/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

### Summary The Registry's HTTP-based namespace verification (`POST /v0/auth/http`, `POST /v0.1/auth/http`) uses `safeDialContext` (`internal/api/handlers/v0/auth/http.go:67-110`) to refuse dialling private/internal addresses when fetching the well-known public-key file from a publisher-supplied domain. The blocklist (`isBlockedIP`, lines 125-133) relies entirely on Go stdlib's `IsLoopback / IsPrivate / IsLinkLocalUnicast / IsMulticast / IsUnspecified` plus a manual CGNAT range. **None of these cover IPv6 6to4 (`2002::/16`), NAT64 (`64:ff9b::/96` and `64:ff9b:1::/48` per RFC 8215), or deprecated site-local (`fec0::/10`)** — all of which encode arbitrary IPv4 in the address bits and tunnel to RFC1918 / cloud-metadata services on dual-stack / NAT64-enabled hosts. This is the same CWE-918 SSRF class fixed in **GHSA-56c3-vfp2-5qqj** on `czlonkowski/n8n-mcp` (CVSS 8.5 HIGH). The remediation pattern is identical: extend the blocklist with the IPv6 prefix families that embed IPv4. The endpoint is **unauthenticated** — it is the login flow itself — so attack complexity is low aside from the host-level routing dependency. Affected: latest `main` HEAD `23f4fda` and current production `v1.7.6` deployment at `https://registry.modelcontextprotocol.io/v0/auth/http`. ### Details #### Vulnerable code `internal/api/handlers/v0/auth/http.go:125-133`: ```go func isBlockedIP(ip net.IP) bool { if ip == nil { return true } return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() || cgnatRange.Contains(ip) } ``` Per Go source (`src/net/ip.go`), the relevant stdlib helpers cover: | Helper | IPv6 coverage | |---|---| | `IsLoopback` | `::1`, IPv4-mapped of 127/8 (via `To4()` fast-path) | | `IsPrivate` | ULA `fc00::/7` only — `ip[0]&0xfe == 0xfc` | | `IsLinkLocalUnicast` | `fe80::/10` only — `ip[1]&0xc0 == 0x80` (NOT `fec0::/10` which is `0xc0`) | | `IsMulticast` | `ff00::/8` | | `IsUnspecified` | `::` | The Registry's blocklist therefore **does not** cover: | Prefix | Defined in | Why dangerous | |---|---|---| | `2002::/16` | RFC 3056 (6to4) | Bits 16-47 embed an arbitrary IPv4 address. `2002:a9fe:a9fe::` is the 6to4 encoding of `169.254.169.254` (AWS / Azure metadata). `2002:0a00:0001::` encodes `10.0.0.1`. On hosts with 6to4 routing or any explicit `2002::/16` route, the dial reaches the embedded IPv4. | | `64:ff9b::/96` | RFC 6052 (NAT64 well-known prefix) | Low 32 bits embed an IPv4 address. `64:ff9b::a9fe:a9fe` translates to `169.254.169.254` on any NAT64-enabled network — which is the **default** in IPv6-only GKE node pools, AWS IPv6-only EC2, Azure IPv6 VMs with NAT64, and DNS64/NAT64 corporate networks. | | `64:ff9b:1::/48` | RFC 8215 (local-use NAT64) | Same tunnelling concern, intended for operator-defined NAT64. | | `fec0::/10` | RFC 3879 (deprecated site-local) | Some BSD / older Linux stacks still honour these for routing into site-local internal networks. | `safeDialContext` resolves DNS once and dials by IP (good — pins against rebinding TOCTOU), but the IP-allowlist gate is the security boundary, and that gate is incomplete. #### Exposure surface `POST /v0/auth/http` (and `POST /v0.1/auth/http`) is registered in `internal/api/handlers/v0/auth/http.go:197-218` and routed unauthenticated in `internal/api/router/v0.go:24,39`: ```go huma.Register(api, huma.Operation{ OperationID: "exchange-http-token...", Method: http.MethodPost, Path: pathPrefix + "/auth/http", Summary: "Exchange HTTP signature for Registry JWT", ... }, func(ctx context.Context, input *HTTPTokenExchangeInput) (...) { response, err := handler.ExchangeToken(ctx, input.Body.Domain, ...) ... }) ``` The handler builds `https://<attacker-domain>/.well-known/mcp-registry-auth` (line 143) and dials via the `safeDialContext`-equipped client. The `domain` parameter is taken verbatim from the unauthenticated POST body. Critical order-of-operations confirmation in `CoreAuthHandler.ExchangeToken` (`internal/api/handlers/v0/auth/common.go:246-265`): 1. `ValidateDomainAndTimestamp(domain, timestamp)` — domain format check (no IP literal, must contain dot) 2. `DecodeAndValidateSignature(signedTimestamp)` — hex decode 3. **`keyFetcher(ctx, domain)`** ← SSRF dial happens here 4. `VerifySignatureWithKeys(...)` ← only AFTER fetch So the SSRF dial fires before any signature verification. Attacker needs only a valid RFC3339 timestamp (±15s window) and any hex string for `signedTimestamp`. ### PoC Tested against `main` HEAD `23f4fda` (`make dev-compose` boots Registry on `localhost:8080`). #### Step 1 — Set up attacker DNS Configure `attacker.example` with the AAAA records: ``` attacker-6to4.example. AAAA 2002:a9fe:a9fe:: ; 6to4 -> 169.254.169.254 attacker-nat64.example. AAAA 64:ff9b::a9fe:a9fe ; NAT64 -> 169.254.169.254 attacker-rfc1918.example. AAAA 64:ff9b::a00:0001 ; NAT64 -> 10.0.0.1 ``` (Equivalent free options: a domain on Cloudflare with manual AAAA, or a `requestbin`-style service with custom DNS.) #### Step 2 — Trigger the dial (no credentials required) ```bash curl -i https://registry.modelcontextprotocol.io/v0/auth/http \ -H 'Content-Type: application/json' \ -d "{\"domain\":\"attacker-nat64.example\",\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"signedTimestamp\":\"00\"}" ``` Timestamp need only be within ±15s of server clock. `signedTimestamp` is any hex string — it is decoded but only verified AFTER `FetchKey` has already dialled. #### Step 3 — Observe On a NAT64-enabled host (default in IPv6-only GKE / AWS IPv6 nodes / Cloudflare WARP), the server-side dial reaches `169.254.169.254:443`. Tcpdump on the registry host confirms the outbound TLS handshake to the embedded IPv4. Where 169.254.169.254 listens on a TLS port (most cloud metadata services do not, but kube-apiserver, internal admin panels, and bespoke IPv4 services do), the connection completes and the response (limited to 4 KiB by `MaxKeyResponseSize`) is consumed as a key candidate. For hosts without 6to4 / NAT64 routing, the dial fails with `no route to host` rather than `refusing to connect to private or loopback address` — proving the gate did not block. The differential error message provides a blind-SSRF oracle for probing internal services for existence / TLS port reachability. #### Expected behaviour after fix `isBlockedIP` should return `true` for any IPv6 address in the prefix families listed above, mirroring the n8n-mcp `isPrivateOrMappedIpv6` helper (GHSA-56c3-vfp2-5qqj patch). Reference implementation: ```go func isBlockedIPv6Prefix(ip net.IP) bool { v6 := ip.To16() if v6 == nil || ip.To4() != nil { return false } // 6to4 (2002::/16) if v6[0] == 0x20 && v6[1] == 0x02 { return true } // NAT64 well-known 64:ff9b::/96 if v6[0] == 0x00 && v6[1] == 0x64 && v6[2] == 0xff && v6[3] == 0x9b && v6[4] == 0 && v6[5] == 0 && v6[6] == 0 && v6[7] == 0 { return true } // NAT64 RFC 8215 local-use 64:ff9b:1::/48 if v6[0] == 0x00 && v6[1] == 0x64 && v6[2] == 0xff && v6[3] == 0x9b && v6[4] == 0x00 && v6[5] == 0x01 { return true } // Site-local fec0::/10 (deprecated, RFC 3879 -- still honoured by some stacks) if v6[0] == 0xfe && (v6[1]&0xc0) == 0xc0 { return true } return false } ``` Then extend the call site: ```go return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() || cgnatRange.Contains(ip) || isBlockedIPv6Prefix(ip) ``` A regression test fixture should set up a stub resolver returning each of the four prefix families and assert that `safeDialContext` returns the "private/loopback" error before any dial. ### Impact CWE: **CWE-918** Server-Side Request Forgery (consistent with parent precedent GHSA-56c3-vfp2-5qqj). CVSS:3.1: matching the n8n-mcp precedent (`AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N` ~= **8.5 HIGH**). AC = High because exploitation depends on the registry host having NAT64 or 6to4 routing — the **default** on IPv6-only and dual-stack cloud network plans (GKE IPv6, AWS IPv6-only EC2, Azure IPv6 VMs with NAT64) but not on plain-IPv4 deployments. Privileges = None (the endpoint is the login flow itself). For the official `https://registry.modelcontextprotocol.io` deployment specifically, this lets an unauthenticated attacker reach any IPv4 address that is routable from the registry's outbound interface — including AWS / GCP / Azure metadata services if hosted on a cloud VM with metadata enabled, internal Kubernetes API servers, internal admin panels, etc. The 4 KiB response cap (`MaxKeyResponseSize`) limits exfiltrated content per request but does not prevent fingerprinting / oracle attacks (status-code differential, response-length differential). Self-hosters running the registry on dual-stack / IPv6-only infrastructure are equally exposed. ### Why this slipped past PR #1227 The April 29 hardening batch (commit `1201cbd`, "security: fix open redirect and add small hardening") explicitly added `safeDialContext` to block "loopback, RFC1918, link-local, multicast, CGNAT, or IP-literal/single-label" addresses. The author correctly identified the IPv4 attack surface and the link-local cloud-metadata vector, but composed the blocklist from Go's per-class stdlib helpers — which collectively miss the IPv6 prefix families that *embed* IPv4. The same gap was caught and fixed in n8n-mcp (GHSA-56c3-vfp2-5qqj). No commits in `git log --since=2026-03-01 internal/api/handlers/v0/auth/http.go` reference 6to4 / NAT64 / site-local. ### Credit Reported by **Matteo Panzeri** (GitHub: **matte1782**).

CVE-2026-44430 Affected Github.com/modelcontextprotocol/registry Versions

1 component(s)
Fixed Versions:
1.7.7
Product / Package Ecosystem Affected Versions Fixed Version
github.com/modelcontextprotocol/registry
Go
< 1.7.7
1.7.7

References

Frequently Asked Questions

What is CVE-2026-44430?
CVE-2026-44430 is a security vulnerability in Github.com/modelcontextprotocol/registry involving HTTP namespace verification dials 6to4 / NAT64 / site-local IPv6 addresses, bypassing private-address allowlist under CWE-918: Server-Side Request Forgery (SSRF).
Which Github.com/modelcontextprotocol/registry versions are affected?
Affected versions include releases prior to 1.7.7 across affected packages: github.com/modelcontextprotocol/registry.
What version fixes CVE-2026-44430?
Upgrading to 1.7.7 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/modelcontextprotocol/registry affected?
Yes, github.com/modelcontextprotocol/registry is affected in releases prior to 1.7.7. Upgrade your dependency to 1.7.7 or later.
What is the CVSS score of CVE-2026-44430?
CVE-2026-44430 has a CVSS v3.1 base score of 4.0 (MODERATE) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N.
What CWE is associated with CVE-2026-44430?
CVE-2026-44430 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check Go dependencies for CVE-2026-44430?
Run the dependency tree command: 'inspect dependencies for github.com/modelcontextprotocol/registry'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-r48c-v28r-pf6v",
  "modified" : "2026-05-15T23:46:20Z",
  "published" : "2026-05-08T17:20:56Z",
  "aliases" : [ "CVE-2026-44430" ],
  "summary" : "MCP Registry has an unauthenticated SSRF: HTTP namespace verification dials 6to4 / NAT64 / site-local IPv6 addresses, bypassing private-address allowlist",
  "details" : "### Summary\n\nThe Registry's HTTP-based namespace verification (`POST /v0/auth/http`, `POST /v0.1/auth/http`) uses `safeDialContext` (`internal/api/handlers/v0/auth/http.go:67-110`) to refuse dialling private/internal addresses when fetching the well-known public-key file from a publisher-supplied domain. The blocklist (`isBlockedIP`, lines 125-133) relies entirely on Go stdlib's `IsLoopback / IsPrivate / IsLinkLocalUnicast / IsMulticast / IsUnspecified` plus a manual CGNAT range. **None of these cover IPv6 6to4 (`2002::/16`), NAT64 (`64:ff9b::/96` and `64:ff9b:1::/48` per RFC 8215), or deprecated site-local (`fec0::/10`)** — all of which encode arbitrary IPv4 in the address bits and tunnel to RFC1918 / cloud-metadata services on dual-stack / NAT64-enabled hosts.\n\nThis is the same CWE-918 SSRF class fixed in **GHSA-56c3-vfp2-5qqj** on `czlonkowski/n8n-mcp` (CVSS 8.5 HIGH). The remediation pattern is identical: extend the blocklist with the IPv6 prefix families that embed IPv4.\n\nThe endpoint is **unauthenticated** — it is the login flow itself — so attack complexity is low aside from the host-level routing dependency.\n\nAffected: latest `main` HEAD `23f4fda` and current production `v1.7.6` deployment at `https://registry.modelcontextprotocol.io/v0/auth/http`.\n\n### Details\n\n#### Vulnerable code\n\n`internal/api/handlers/v0/auth/http.go:125-133`:\n\n```go\nfunc isBlockedIP(ip net.IP) bool {\n    if ip == nil {\n        return true\n    }\n    return ip.IsLoopback() || ip.IsPrivate() ||\n        ip.IsLinkLocalUnicast() || ip.IsMulticast() ||\n        ip.IsUnspecified() ||\n        cgnatRange.Contains(ip)\n}\n```\n\nPer Go source (`src/net/ip.go`), the relevant stdlib helpers cover:\n\n| Helper | IPv6 coverage |\n|---|---|\n| `IsLoopback` | `::1`, IPv4-mapped of 127/8 (via `To4()` fast-path) |\n| `IsPrivate` | ULA `fc00::/7` only — `ip[0]&0xfe == 0xfc` |\n| `IsLinkLocalUnicast` | `fe80::/10` only — `ip[1]&0xc0 == 0x80` (NOT `fec0::/10` which is `0xc0`) |\n| `IsMulticast` | `ff00::/8` |\n| `IsUnspecified` | `::` |\n\nThe Registry's blocklist therefore **does not** cover:\n\n| Prefix | Defined in | Why dangerous |\n|---|---|---|\n| `2002::/16` | RFC 3056 (6to4) | Bits 16-47 embed an arbitrary IPv4 address. `2002:a9fe:a9fe::` is the 6to4 encoding of `169.254.169.254` (AWS / Azure metadata). `2002:0a00:0001::` encodes `10.0.0.1`. On hosts with 6to4 routing or any explicit `2002::/16` route, the dial reaches the embedded IPv4. |\n| `64:ff9b::/96` | RFC 6052 (NAT64 well-known prefix) | Low 32 bits embed an IPv4 address. `64:ff9b::a9fe:a9fe` translates to `169.254.169.254` on any NAT64-enabled network — which is the **default** in IPv6-only GKE node pools, AWS IPv6-only EC2, Azure IPv6 VMs with NAT64, and DNS64/NAT64 corporate networks. |\n| `64:ff9b:1::/48` | RFC 8215 (local-use NAT64) | Same tunnelling concern, intended for operator-defined NAT64. |\n| `fec0::/10` | RFC 3879 (deprecated site-local) | Some BSD / older Linux stacks still honour these for routing into site-local internal networks. |\n\n`safeDialContext` resolves DNS once and dials by IP (good — pins against rebinding TOCTOU), but the IP-allowlist gate is the security boundary, and that gate is incomplete.\n\n#### Exposure surface\n\n`POST /v0/auth/http` (and `POST /v0.1/auth/http`) is registered in `internal/api/handlers/v0/auth/http.go:197-218` and routed unauthenticated in `internal/api/router/v0.go:24,39`:\n\n```go\nhuma.Register(api, huma.Operation{\n    OperationID: \"exchange-http-token...\",\n    Method:      http.MethodPost,\n    Path:        pathPrefix + \"/auth/http\",\n    Summary:     \"Exchange HTTP signature for Registry JWT\",\n    ...\n}, func(ctx context.Context, input *HTTPTokenExchangeInput) (...) {\n    response, err := handler.ExchangeToken(ctx, input.Body.Domain, ...)\n    ...\n})\n```\n\nThe handler builds `https://<attacker-domain>/.well-known/mcp-registry-auth` (line 143) and dials via the `safeDialContext`-equipped client. The `domain` parameter is taken verbatim from the unauthenticated POST body.\n\nCritical order-of-operations confirmation in `CoreAuthHandler.ExchangeToken` (`internal/api/handlers/v0/auth/common.go:246-265`):\n\n1. `ValidateDomainAndTimestamp(domain, timestamp)` — domain format check (no IP literal, must contain dot)\n2. `DecodeAndValidateSignature(signedTimestamp)` — hex decode\n3. **`keyFetcher(ctx, domain)`** ← SSRF dial happens here\n4. `VerifySignatureWithKeys(...)` ← only AFTER fetch\n\nSo the SSRF dial fires before any signature verification. Attacker needs only a valid RFC3339 timestamp (±15s window) and any hex string for `signedTimestamp`.\n\n### PoC\n\nTested against `main` HEAD `23f4fda` (`make dev-compose` boots Registry on `localhost:8080`).\n\n#### Step 1 — Set up attacker DNS\n\nConfigure `attacker.example` with the AAAA records:\n\n```\nattacker-6to4.example.       AAAA  2002:a9fe:a9fe::         ; 6to4 -> 169.254.169.254\nattacker-nat64.example.      AAAA  64:ff9b::a9fe:a9fe       ; NAT64 -> 169.254.169.254\nattacker-rfc1918.example.    AAAA  64:ff9b::a00:0001        ; NAT64 -> 10.0.0.1\n```\n\n(Equivalent free options: a domain on Cloudflare with manual AAAA, or a `requestbin`-style service with custom DNS.)\n\n#### Step 2 — Trigger the dial (no credentials required)\n\n```bash\ncurl -i https://registry.modelcontextprotocol.io/v0/auth/http \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"domain\\\":\\\"attacker-nat64.example\\\",\\\"timestamp\\\":\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\",\\\"signedTimestamp\\\":\\\"00\\\"}\"\n```\n\nTimestamp need only be within ±15s of server clock. `signedTimestamp` is any hex string — it is decoded but only verified AFTER `FetchKey` has already dialled.\n\n#### Step 3 — Observe\n\nOn a NAT64-enabled host (default in IPv6-only GKE / AWS IPv6 nodes / Cloudflare WARP), the server-side dial reaches `169.254.169.254:443`. Tcpdump on the registry host confirms the outbound TLS handshake to the embedded IPv4. Where 169.254.169.254 listens on a TLS port (most cloud metadata services do not, but kube-apiserver, internal admin panels, and bespoke IPv4 services do), the connection completes and the response (limited to 4 KiB by `MaxKeyResponseSize`) is consumed as a key candidate.\n\nFor hosts without 6to4 / NAT64 routing, the dial fails with `no route to host` rather than `refusing to connect to private or loopback address` — proving the gate did not block. The differential error message provides a blind-SSRF oracle for probing internal services for existence / TLS port reachability.\n\n#### Expected behaviour after fix\n\n`isBlockedIP` should return `true` for any IPv6 address in the prefix families listed above, mirroring the n8n-mcp `isPrivateOrMappedIpv6` helper (GHSA-56c3-vfp2-5qqj patch). Reference implementation:\n\n```go\nfunc isBlockedIPv6Prefix(ip net.IP) bool {\n    v6 := ip.To16()\n    if v6 == nil || ip.To4() != nil {\n        return false\n    }\n    // 6to4 (2002::/16)\n    if v6[0] == 0x20 && v6[1] == 0x02 {\n        return true\n    }\n    // NAT64 well-known 64:ff9b::/96\n    if v6[0] == 0x00 && v6[1] == 0x64 && v6[2] == 0xff && v6[3] == 0x9b &&\n       v6[4] == 0 && v6[5] == 0 && v6[6] == 0 && v6[7] == 0 {\n        return true\n    }\n    // NAT64 RFC 8215 local-use 64:ff9b:1::/48\n    if v6[0] == 0x00 && v6[1] == 0x64 && v6[2] == 0xff && v6[3] == 0x9b &&\n       v6[4] == 0x00 && v6[5] == 0x01 {\n        return true\n    }\n    // Site-local fec0::/10 (deprecated, RFC 3879 -- still honoured by some stacks)\n    if v6[0] == 0xfe && (v6[1]&0xc0) == 0xc0 {\n        return true\n    }\n    return false\n}\n```\n\nThen extend the call site:\n\n```go\nreturn ip.IsLoopback() || ip.IsPrivate() ||\n    ip.IsLinkLocalUnicast() || ip.IsMulticast() ||\n    ip.IsUnspecified() ||\n    cgnatRange.Contains(ip) ||\n    isBlockedIPv6Prefix(ip)\n```\n\nA regression test fixture should set up a stub resolver returning each of the four prefix families and assert that `safeDialContext` returns the \"private/loopback\" error before any dial.\n\n### Impact\n\nCWE: **CWE-918** Server-Side Request Forgery (consistent with parent precedent GHSA-56c3-vfp2-5qqj).\n\nCVSS:3.1: matching the n8n-mcp precedent (`AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N` ~= **8.5 HIGH**). AC = High because exploitation depends on the registry host having NAT64 or 6to4 routing — the **default** on IPv6-only and dual-stack cloud network plans (GKE IPv6, AWS IPv6-only EC2, Azure IPv6 VMs with NAT64) but not on plain-IPv4 deployments. Privileges = None (the endpoint is the login flow itself).\n\nFor the official `https://registry.modelcontextprotocol.io` deployment specifically, this lets an unauthenticated attacker reach any IPv4 address that is routable from the registry's outbound interface — including AWS / GCP / Azure metadata services if hosted on a cloud VM with metadata enabled, internal Kubernetes API servers, internal admin panels, etc. The 4 KiB response cap (`MaxKeyResponseSize`) limits exfiltrated content per request but does not prevent fingerprinting / oracle attacks (status-code differential, response-length differential).\n\nSelf-hosters running the registry on dual-stack / IPv6-only infrastructure are equally exposed.\n\n### Why this slipped past PR #1227\n\nThe April 29 hardening batch (commit `1201cbd`, \"security: fix open redirect and add small hardening\") explicitly added `safeDialContext` to block \"loopback, RFC1918, link-local, multicast, CGNAT, or IP-literal/single-label\" addresses. The author correctly identified the IPv4 attack surface and the link-local cloud-metadata vector, but composed the blocklist from Go's per-class stdlib helpers — which collectively miss the IPv6 prefix families that *embed* IPv4. The same gap was caught and fixed in n8n-mcp (GHSA-56c3-vfp2-5qqj). No commits in `git log --since=2026-03-01 internal/api/handlers/v0/auth/http.go` reference 6to4 / NAT64 / site-local.\n\n### Credit\n\nReported by **Matteo Panzeri** (GitHub: **matte1782**).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N"
  }, {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:N/SA:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/modelcontextprotocol/registry"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.7.7"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/modelcontextprotocol/registry/security/advisories/GHSA-r48c-v28r-pf6v"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-44430"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/modelcontextprotocol/registry/pull/1250"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/modelcontextprotocol/registry/commit/f5f40bd98084466eaf18fe48ea62a0d534caa774"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/modelcontextprotocol/registry"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/modelcontextprotocol/registry/releases/tag/v1.7.7"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-918" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-08T17:20:56Z",
    "nvd_published_at" : "2026-05-14T21:16:46Z"
  }
}