Intelligence

MCPHub's SSRF Guard Denied the IPv4 Address. A Transition Encoding Reached It Anyway.

GHSA-pr4x-3pc7-2fhw, published 23 August 2026 and formally assigning CVE-2026-79749, documents MCPHub's isBlockedIpv6 destination guard correctly rejecting loopback, link local and unique local addresses while never checking three IPv6 transition families, NAT64, 6to4 and Teredo, each capable of carrying an embedded private IPv4 address past the same guard unrecognized. Moona Intelligence read the affected src/utils/ssrf.ts directly, before and after remediation commit d382b36, merged in pull request 1069 as a56b4a17e25e2ebf09323a10882e1f83e445b2a8, and shipped in MCPHub 1.0.32.

Event analysed: . This analysis was published on 6 September 2026.

MCPHub's SSRF guard already denied a caller supplied loopback or private IPv4 address. Whose authority decided that the identical address, reached through a different IPv6 encoding, was a different question?

Nobody's. GitHub Security Advisory GHSA-pr4x-3pc7-2fhw, read directly from the repository's own advisory page, formally assigns CVE-2026-79749 on 23 August 2026, rated High severity under CWE-918, Server Side Request Forgery, against MCPHub through version 1.0.31, patched in 1.0.32. The advisory states plainly what this record independently confirmed by reading src/utils/ssrf.ts at the commit immediately before the fix: isBlockedIpv6 already rejected the unspecified and loopback addresses (::, ::1), the fe80::/10 link local range, the fc00::/7 unique local range, and both IPv4 mapped (::ffff:a.b.c.d) and IPv4 compatible (::a.b.c.d) forms, and this record confirmed each of those checks ran correctly against the address forms MCPHub's own existing test suite exercised. What the same function never checked was whether an address fell inside one of three IPv6 transition prefixes capable of embedding an IPv4 address inside an IPv6 literal: the NAT64 well known prefix, 64:ff9b::/96, the deprecated 6to4 prefix, 2002::/16, and the Teredo prefix, 2001:0::/32. A caller authorized to name the URL an MCP server connection dials, but not authorized to reach an internal address directly, could instead name that same internal address encoded inside one of these three prefixes, and MCPHub's own guard, whose bigint comparison treats an IPv6 literal as a single numeric value rather than as a wrapper around a possible IPv4 payload, had no rule that recognized the wrapper as the address it carried. Remediation commit d382b36, read by this record at its equivalent commit on MCPHub's main branch, a56b4a17e25e2ebf09323a10882e1f83e445b2a8, adds exactly the three missing checks the advisory names, merged in pull request 1069 and shipped in MCPHub 1.0.32, published 23 August 2026.

Read MCPHub's own affected function and the finding is not that a destination guard was missing. isBlockedIpv6 existed, ran on every outbound connection MCPHub's own assertSafeUrl function gated, and this record confirmed directly that it correctly rejected the address forms its own test suite already exercised: the unspecified and loopback addresses, the fe80::/10 link local range, the fc00::/7 unique local range, and both the IPv4 mapped and the deprecated IPv4 compatible embeddings. Three further ways of writing an IPv6 address, each capable of carrying a private IPv4 address to the same effective destination, were never inside that function's own equivalence class.

What the advisory and the source together establish

GitHub Security Advisory GHSA-pr4x-3pc7-2fhw, fetched directly from samanhappy/mcphub's own repository advisory page, states its title as "SSRF Guard Bypass via IPv6 Transition Addresses in URL Validation", formally assigns CVE-2026-79749, rates the finding High severity under CWE-918, Server Side Request Forgery, and states the affected range as MCPHub through version 1.0.31, patched in 1.0.32. The advisory's own description names the mechanism precisely: isBlockedIpv6 implements incomplete IPv6 address validation, checking only loopback, link local, unique local and IPv4 mapped forms, and fails to validate three IPv6 transition address families, NAT64 at 64:ff9b::/96, 6to4 at 2002::/16, and Teredo at 2001:0::/32. The advisory states the consequence directly: an attacker already authorized to configure an MCP server connection can encode a private IPv4 address using one of these transition formats to bypass SSRF protection, reaching internal infrastructure including cloud metadata endpoints such as 169.254.169.254 on a network where the relevant transition mechanism is enabled.

What this record verified by reading the affected source directly, rather than by relying on the advisory's own prose alone: the commit immediately before the fix, dd5c7d7a109fbe4b91aaeb3a77b3cc863c5aa3d0, contains exactly the five checks the advisory describes as present (unspecified and loopback, fe80::/10, fc00::/7, and the two IPv4 embedding forms) and no check for any of the three named transition prefixes. The fix commit, a56b4a17e25e2ebf09323a10882e1f83e445b2a8, is the on branch equivalent of the pull request's own d382b36, and its own commit message names GHSA-pr4x-3pc7-2fhw directly.

How a transition prefix carries a blocked address past a check built for one representation

isBlockedIpv6 takes a parsed IPv6 address as a single 128 bit integer and compares it, through bit shifts, against the numeric ranges its own author wrote checks for. That approach is exact wherever a check exists for the range being tested. NAT64, 6to4 and Teredo are each a standardized way of embedding a 32 bit IPv4 address inside the low order bits of an IPv6 address that also carries a fixed, well known high order prefix identifying which transition mechanism produced it. An address inside 64:ff9b::/96 carries an IPv4 address in its final 32 bits; the same is true, with different prefixes and different embedding conventions, for 2002::/16 and 2001:0::/32. None of these three high order prefixes matched any range isBlockedIpv6 already tested for, so an address built this way fell straight through every existing check to the function's own final return false, the same outcome a genuinely public IPv6 address would produce. The guard's own logic was not wrong about any range it checked; it simply never asked whether the address in front of it was a second, valid way of writing an address it already knew to block.

Three named families, and two further ranges the same commit also closes

The advisory's own description of the vulnerability names three address families: NAT64's well known prefix, 6to4, and Teredo. This record reads the fix commit's diff directly and confirms it adds five checks, not three: the three the advisory names, plus a check for 64:ff9b:1::/48, the separate, locally assigned NAT64 prefix distinct from the well known one, and checks for fec0::/10, deprecated site local addressing, and ff00::/8, multicast. This record treats the three families the advisory's own description names, NAT64's well known prefix, 6to4 and Teredo, as CVE-2026-79749's own scope, and treats the local use NAT64 prefix, site local addressing and multicast as hardening bundled into the same commit rather than as address families the advisory itself attributes to this vulnerability. Multicast, an address a caller cannot ordinarily use to complete a normal TCP connection at all, is a materially different risk shape from an address that can carry a private IPv4 host reachable on the caller's own network; this record does not treat the advisory's silence on those two ranges as an oversight in the advisory, but keeps the distinction visible rather than folding five fixed checks into one three family description.

One pull request, six advisories

Pull request 1069, titled by its own author "fix(security): close open security advisory findings (SSRF, authz, OAuth PKCE)" and merged as 4ed79f250ef7e9adac52e8028274c97d49aff065, bundles six separate commits, each closing a distinct GitHub Security Advisory: the IPv6 transition address fix this record documents, a fix routing external OpenAPI reference resolution through the same SSRF guard, an authorization fix for built in prompt and resource mutations, an ownership filtering fix for template export, an OAuth client authentication and PKCE enforcement fix, and a startup warning when an operator disables authentication. This record's own claims are scoped to the one commit, d382b36 on the pull request's own branch and a56b4a17e25e2ebf09323a10882e1f83e445b2a8 on MCPHub's main branch, that GHSA-pr4x-3pc7-2fhw and CVE-2026-79749 name. The other five commits in the same pull request close five other advisories this record did not independently verify and does not attribute to this CVE.

Not CVE-2026-79747

The same repository's advisory page lists a second SSRF finding published the same day, GHSA-9wx9-prgc-gmjr, formally assigning CVE-2026-79747, CVSS 3.1 base score 7.1, also under CWE-918, also patched in MCPHub 1.0.32. Read directly, that advisory describes a different mechanism entirely: a non administrative authenticated user could register an MCP server pointing at an arbitrary URL at all, reaching it through either a reflected path (a malicious OpenAPI schema's own servers URL returning its response body to the caller) or a blind path (an SSE or HTTP transport connection dialing the caller named URL directly), because only stdio and command based servers required admin privilege, while URL based servers carried no equivalent authorization check. That is an authorization gap over who may configure a destination at all. CVE-2026-79749 concerns a narrower, later question: once a caller is authorized to name a destination, and a guard exists to keep that destination out of blocked address space, whether every representation of a blocked address is actually inside that guard's own recognized scope. This record keeps the two CVEs, the two GHSA ids and the two mechanisms separate, and did not independently verify which commit fixes CVE-2026-79747, since that fix falls outside what this record set out to establish.

What the current source shows beyond the named fix

This record read MCPHub's current src/utils/ssrf.ts in full, not only the diff the fix commit applies. assertSafeUrl, the function every outbound MCPHub connection is gated through, parses the target URL, rejects any scheme other than http and https, and for a hostname that is not itself an IP literal, resolves it through Node's own dns.lookup with the all option set and evaluates isBlockedIp, the function wrapping isBlockedIpv4Number and isBlockedIpv6, against every address the resolution returns, not only the first. A hostname that fails to resolve, or an address that is not a literal IP and cannot be parsed at all, is rejected rather than passed through, a fail closed default this record confirmed directly in the function's own control flow. The same file also exports createRedirectValidatingFetch, which this record confirmed was already present at the fix commit rather than introduced by it: it wraps an underlying fetch so that a 3xx response's Location header is resolved and passed back through assertSafeUrl before the next hop is followed, up to five hops, rather than allowing an underlying HTTP client to auto follow a redirect straight to an address nothing has evaluated. A further commit, 67738d4e209f947b3973e3b9d01ea2379e059ffb, dated 1 September 2026 and read directly by this record, extends createRedirectValidatingFetch to accept the same lookup function its caller already supplies to the initial assertSafeUrl call, so that one OAuth client id fetch path's own DNS resolution behavior, including a test injected override, applies consistently across every redirect hop rather than only the first request. This record treats that commit as later evidence of the same principle, that a destination must be re evaluated at each transformation a request undergoes, and states plainly that it is dated after CVE-2026-79749's own fix, ships outside pull request 1069, and is not part of what GHSA-pr4x-3pc7-2fhw or CVE-2026-79749 describes.

The Authority Provenance ledger

Authority grantor. The MCPHub operator who decides which authenticated users may register an MCP server connection naming an arbitrary URL, and who relies on src/utils/ssrf.ts to keep that URL's effective destination out of address space the operator never intended to expose.

Caller identity. An authenticated MCPHub user already permitted to configure a URL based MCP server or OpenAPI connection. The advisory states this precondition directly; this record did not independently verify which MCPHub role or roles satisfy it.

Caller mandate. Whatever mandate admits a caller to name the URL a server connection dials at all. Nothing in the advisory or the affected source describes that mandate as extending to a specific, named internal address; the guard exists precisely because it is not meant to.

Intermediary. MCPHub's own server process, through assertSafeUrl and the isBlockedIp and isBlockedIpv6 checks it calls before dialing a caller named URL or a caller named OpenAPI server URL.

Intermediary network authority. Whatever loopback, private network and, on a network where the relevant transition mechanism is enabled, cloud metadata destination the MCPHub server process could reach from wherever the operator deployed it.

Downstream target. Any address expressible inside the NAT64 well known prefix, the 6to4 prefix or the Teredo prefix, evidenced by this record's own direct read of the pre fix and post fix isBlockedIpv6, and by the advisory's own statement that the mechanism can reach cloud metadata infrastructure.

Failed limit. isBlockedIpv6 evaluated the requested address's IPv6 representation, and only that representation, against a fixed set of numeric ranges that did not include three standardized ways of writing the same effective, prohibited address, confirmed directly against the pre fix source.

Inherited assumption. That a destination check written against the address forms a test suite exercises has thereby evaluated every representation capable of producing the same effective destination, the same substitution this desk already reads in Postgres MCP Pro's SafeSqlDriver, evidenced there at a syntax tree traversal boundary rather than at an address canonicalization boundary.

Credential binding. Not the mechanism this record describes; no credential forwarding is at issue in the finding GHSA-pr4x-3pc7-2fhw names.

Destination binding. Absent for three transition encodings at the pre fix commit, confirmed directly; present for those three encodings, plus two further hardened ranges, at the fix commit and in current source.

Recovery path. Remediation commit d382b36, merged as a56b4a17e25e2ebf09323a10882e1f83e445b2a8 in pull request 1069, shipped in MCPHub 1.0.32, published 23 August 2026. This record confirmed the merged commit is present on MCPHub's current default branch.

Exploitation. Unknown. Neither the repository's own advisory page nor anything else available to this record states the vulnerability has been exploited against a production MCPHub deployment.

Provenance evidence quality. Strong for the mechanism: this record read the affected source directly at the commit immediately before the fix and at the fix commit itself, confirming the exact five checks the fix adds and the exact absence each one closes. Strong for the advisory's own stated metadata, read directly from the repository's own advisory page. Weaker for the CVE Program's own record: cve.org, nvd.nist.gov and osv.dev were each blocked to direct fetch in this session on every attempt, and the aggregated GitHub Advisory Database search at github.com/advisories did not return a result for CVE-2026-79749 at the time of this record, so this record cannot independently confirm the CVE Program's own publication date against the repository advisory page's own 23 August 2026 date, and states any later CVE Program publication date reported elsewhere as unverified by this record rather than as confirmed.

Where this sits in the pattern

Postgres MCP Pro's SafeSqlDriver already shows this desk the general shape a correctly written check can still fail to close: an allow list that demonstrably works wherever a function call reaches it through the representation the check's own author tested, left unreached by the identical function call nested one container deeper in a different, equally legitimate syntax tree shape. MCPHub's isBlockedIpv6 is the same shape at a different boundary: a numeric range check that worked correctly against every address form its own author tested, left unreached by three standardized alternate ways of writing an address inside the same range. OGX's own advisory showed a boundary with no destination check running at all on one caller controlled path, despite an equivalent, working check already gating two sibling paths in the same codebase; MCPHub's finding is narrower still, since a check did run on the one path in question and simply had an incomplete view of what counted as the address it was checking. Grafana's own advisory for CVE-2026-19516 documents a third variant, a caller supplied destination header reaching an outbound request with no constraint at all until a later fix added one. Read together, the pattern across all three is that a working destination guard is necessary and, on the evidence these three vulnerabilities each independently supply, still not sufficient on its own: what the guard recognizes as the resource it is evaluating has to match, exactly, the resource the request will actually reach.

What this record does not establish

This record does not claim CVE-2026-79749 has been exploited against a real MCPHub deployment, that a specific numeric CVSS score exists for this CVE beyond the advisory's own High severity label, that this record's stated 23 August 2026 GHSA publication date matches a CVE Program record this session could not directly read, that pull request 1069's other five bundled fixes were independently verified by this record, or that a56b4a17e25e2ebf09323a10882e1f83e445b2a8 and d382b36 are the same commit object rather than the same change carried by a rebase merge that alters the commit hash while preserving its message and diff. Where the evidence available to this record does not establish a fact, this record states it as unknown rather than inferring it from the pattern this weakness class already shows elsewhere.

Sources

This analysis interprets third-party reporting, research and announcements. Moona is not the original reporter of the underlying events.

[1]
SSRF Guard Bypass via IPv6 Transition Addresses in URL Validation (CVE-2026-79749)
samanhappy/mcphub (GitHub Security Advisories) · 23 August 2026 · Primary source
[3]
fix(security): close open security advisory findings (SSRF, authz, OAuth PKCE) (Pull Request 1069)
samanhappy/mcphub (GitHub Pull Requests) · 23 August 2026 · Primary source
[4]
[5]
src/utils/ssrf.ts at commit dd5c7d7 (isBlockedIpv6 immediately before the fix)
samanhappy/mcphub (GitHub, source) · Primary source
[6]
src/utils/ssrf.ts at commit a56b4a1 (isBlockedIpv6 after the fix)
samanhappy/mcphub (GitHub, source) · Primary source
[8]
fix: propagate SSRF DNS lookup to redirect validation (commit 67738d4, 1 September 2026)
samanhappy/mcphub (GitHub, commit) · 1 September 2026 · Primary source
[9]
MCPHub v1.0.32
samanhappy/mcphub (GitHub Releases) · 23 August 2026 · Primary source
[10]
CVE-2026-79749
CVE Program (cve.org) · Regulatory source

Protocol evidence

This record does not assess these architectures. The connection runs through the Risk Registry requirement each one bears on, and these published authority architectures are what the evidence says about that requirement.

Protocol evidence related through AEW-020 An operation blocked in one syntactic form is authorized in an equivalent one

  • Supports requirement

    Agent Control Standard (ACS)

    OWASP GenAI Security Project, originally Zenity

    Requirement The specification does not guarantee complete mediation of every consequential action a host can take

    ACS's own conformance document states directly that the specification does not guarantee complete mediation of every consequential action a host can take, and that a primitive bypassing its own toolCallRequest hook is invisible to policy. Postgres MCP Pro's restricted mode is concrete evidence for exactly the concern that admission names in the abstract: a mediation point that is present, correctly implemented and independently confirmed to work against one representation of a consequential operation still does not guarantee coverage of every representation the same underlying grammar can produce for it. This entry's own execution of the real, patched and unpatched validator against the same four functions supports the requirement that policy coverage be verified against the full range of representations a parser can produce, not only the one a mediation point's own author tested against. MCPHub's isBlockedIpv6 is the same admission read against a destination guard rather than a tool call hook: this entry's own direct reading of the pre fix and post fix source confirms a mediation point that correctly and completely covered every address form its own test suite exercised still left three standardized transition encodings of the same prohibited resource unmediated, supporting the requirement that coverage be verified against every representation an addressing standard can produce, not only the ones a guard's own author tested against.

    This record is the cited evidence for this relationship.

    View protocol evidence

  • Supports requirement

    Verifiable Attenuated Delegation for AI Agent Chains (draft-asor-wimse-agent-delegation-chain)

    Rafael Asor, Attenu

    Requirement A verifier encountering an unrecognized constraint type must deny rather than treat it as unconstrained

    This draft's own verified requirement is that a verifier encountering an unrecognized constraint type must deny rather than treat it as unconstrained, so that a newer, unrecognized restriction is never silently read as no restriction at all. Postgres MCP Pro's SafeSqlDriver shows the inverse failure at a different layer, its own recursive AST traversal encountering a container it does not open, the tuple nested inside RangeFunction's own functions attribute, and treating what it never inspected as though it carried nothing requiring a check, rather than denying by default. This entry's own execution of the real validator confirms the same allow list check works correctly wherever the traversal does open the container it needs to, which is exactly why an unopened one silently passing is the more precise finding here, direct evidence for why a verifier's default posture toward a structure it does not fully parse matters generally, not only for this one draft's own constraint objects. MCPHub's isBlockedIpv6 supports the same requirement at a resource identity boundary rather than a syntax tree boundary: an IPv6 address encoded through the NAT64, 6to4 or Teredo transition prefixes was an unrecognized representation isBlockedIpv6 silently treated as unconstrained, returning false rather than denying by default, until the fix taught the function to recognize those three encodings as the blocked addresses they carry.

    This record is the cited evidence for this relationship.

    View protocol evidence

Related Intelligence

All Intelligence Records →