VYPR
Medium severity4.0GHSA Advisory· Published May 14, 2026· Updated May 15, 2026

CVE-2026-44430

CVE-2026-44430

Description

The MCP Registry provides MCP clients with a list of MCP servers, like an app store for MCP servers. Prior to 1.7.7, 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 vulnerability is fixed in 1.7.7.

Affected products

1

Patches

1
f5f40bd98084

fix(auth/http): extend SSRF blocklist to IPv6 6to4/NAT64/site-local prefixes (#1250)

https://github.com/modelcontextprotocol/registryRadoslav DimitrovMay 4, 2026via ghsa
2 files changed · +73 7
  • internal/api/handlers/v0/auth/http.go+45 7 modified
    @@ -109,27 +109,65 @@ func safeDialContext(ctx context.Context, network, addr string) (net.Conn, error
     	return nil, fmt.Errorf("dial %s: all resolved public addresses failed: %w", host, lastErr)
     }
     
    +// mustCIDR parses a CIDR literal at init; panics on malformed input so a
    +// typo surfaces at startup rather than letting a nil *IPNet through to the
    +// blocklist check (which would silently fail-open).
    +func mustCIDR(s string) *net.IPNet {
    +	_, n, err := net.ParseCIDR(s)
    +	if err != nil {
    +		panic(fmt.Sprintf("auth: invalid CIDR %q: %v", s, err))
    +	}
    +	return n
    +}
    +
     // cgnatRange covers RFC 6598 Carrier-Grade NAT (100.64.0.0/10), which the
     // stdlib does not classify via any Is* helper but is reachable on some
     // cloud / mobile networks where it shadows internal infrastructure.
    -var cgnatRange = func() *net.IPNet {
    -	_, n, _ := net.ParseCIDR("100.64.0.0/10")
    -	return n
    -}()
    +var cgnatRange = mustCIDR("100.64.0.0/10")
    +
    +// blockedIPv6Prefixes are IPv6 ranges that either embed an arbitrary IPv4
    +// address (and therefore tunnel into RFC1918 / cloud-metadata space on
    +// hosts with the corresponding routing) or are routed into site-local
    +// internal networks. None of these are caught by Go's per-class Is*
    +// helpers.
    +//
    +//	2002::/16       RFC 3056 6to4 — bits 16-47 are an IPv4 address
    +//	64:ff9b::/96    RFC 6052 NAT64 well-known — low 32 bits are IPv4
    +//	64:ff9b:1::/48  RFC 8215 NAT64 local-use — same IPv4-embedding shape
    +//	fec0::/10       RFC 3879 site-local (deprecated, still routed by some
    +//	                stacks)
    +var blockedIPv6Prefixes = []*net.IPNet{
    +	mustCIDR("2002::/16"),
    +	mustCIDR("64:ff9b::/96"),
    +	mustCIDR("64:ff9b:1::/48"),
    +	mustCIDR("fec0::/10"),
    +}
     
     // isBlockedIP reports whether an IP must not be dialled by the namespace
     // verification fetcher. Covers loopback (127/8, ::1), RFC1918 + ULA via
     // IsPrivate, link-local (169.254/16, fe80::/10 — includes cloud metadata
     // 169.254.169.254), unspecified (0.0.0.0, ::), all multicast (admin-scoped
    -// 239/8 and ff00::/8 in addition to link-local-multicast), and CGNAT.
    +// 239/8 and ff00::/8 in addition to link-local-multicast), CGNAT, and
    +// IPv6 prefix families that tunnel to or embed arbitrary IPv4 addresses
    +// (see blockedIPv6Prefixes). IPv4-mapped IPv6 (::ffff:0:0/96) is handled
    +// implicitly: the stdlib Is* helpers honour the To4() fast-path, so e.g.
    +// ::ffff:10.0.0.1 is correctly classified as IsPrivate.
     func isBlockedIP(ip net.IP) bool {
     	if ip == nil {
     		return true
     	}
    -	return ip.IsLoopback() || ip.IsPrivate() ||
    +	if ip.IsLoopback() || ip.IsPrivate() ||
     		ip.IsLinkLocalUnicast() || ip.IsMulticast() ||
     		ip.IsUnspecified() ||
    -		cgnatRange.Contains(ip)
    +		cgnatRange.Contains(ip) {
    +		return true
    +	}
    +	for _, p := range blockedIPv6Prefixes {
    +		if p.Contains(ip) {
    +			return true
    +		}
    +	}
    +	return false
     }
     
     // NewDefaultHTTPKeyFetcherWithClient creates a new HTTP key fetcher with a custom HTTP client.
    
  • internal/api/handlers/v0/auth/http_internal_test.go+28 0 modified
    @@ -30,10 +30,38 @@ func TestIsBlockedIP(t *testing.T) {
     		// Blocked — Carrier-Grade NAT (RFC 6598)
     		{"100.64.0.1", true},
     		{"100.127.255.254", true},
    +		// Blocked — IPv6 6to4 (RFC 3056 2002::/16); bits 16-47 are an
    +		// arbitrary IPv4 address, so 2002:a9fe:a9fe:: tunnels to
    +		// 169.254.169.254 and 2002:0a00:0001:: tunnels to 10.0.0.1.
    +		{"2002:a9fe:a9fe::", true},
    +		{"2002:0a00:0001::", true},
    +		{"2002::1", true},
    +		// Blocked — IPv6 NAT64 well-known prefix (RFC 6052 64:ff9b::/96);
    +		// low 32 bits embed an IPv4 address.
    +		{"64:ff9b::a9fe:a9fe", true},
    +		{"64:ff9b::a00:1", true},
    +		// Blocked — IPv6 NAT64 local-use prefix (RFC 8215 64:ff9b:1::/48).
    +		{"64:ff9b:1::1", true},
    +		{"64:ff9b:1:abcd::", true},
    +		// Blocked — IPv6 deprecated site-local (RFC 3879 fec0::/10);
    +		// still routed into internal networks by some stacks.
    +		{"fec0::1", true},
    +		{"feff::1", true},
    +		// Blocked — IPv4-mapped IPv6 (::ffff:0:0/96) inherits the
    +		// classification of the wrapped IPv4 via To4() fast-path; covered
    +		// here as an explicit regression guard.
    +		{"::ffff:127.0.0.1", true},
    +		{"::ffff:10.0.0.1", true},
    +		{"::ffff:169.254.169.254", true},
     		// Allowed — public
     		{"1.1.1.1", false},
     		{"8.8.8.8", false},
     		{"2606:4700:4700::1111", false},
    +		{"2001:4860:4860::8888", false},
    +		// Allowed — just outside the new IPv6 blocks
    +		{"2001::1", false},    // outside 2002::/16
    +		{"2003::1", false},    // outside 2002::/16 on the other side
    +		{"64:ff9c::1", false}, // outside 64:ff9b::/96
     		// Allowed — outside CGNAT range
     		{"100.63.255.255", false},
     		{"100.128.0.1", false},
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

6

News mentions

0

No linked articles in our index yet.