zed-industries/zed · error
OAuth endpoint must not point to IPv6 unique-local address:
Error message
OAuth endpoint must not point to IPv6 unique-local address: {} What it means
The final IPv6 guard in validate_oauth_url blocks unique-local addresses (fc00::/7), detected by masking the first segment with 0xfe00 and comparing to 0xfc00 because std's is_unique_local() is nightly-only. These are the IPv6 equivalents of RFC 1918 private ranges (fd00::/8 commonly used for internal networks), so the same SSRF rationale applies as for private IPv4.
Source
Thrown at crates/context_server/src/oauth.rs:115
|| mapped_v4.is_unspecified()
{
bail!(
"OAuth endpoint must not point to private/reserved IP: ::ffff:{}",
mapped_v4
);
}
}
if ip.is_unspecified() || ip.is_multicast() {
bail!(
"OAuth endpoint must not point to reserved IPv6 address: {}",
ip
);
}
// IPv6 Unique Local Addresses (fc00::/7). is_unique_local() is
// nightly-only, so check the prefix manually.
if (ip.segments()[0] & 0xfe00) == 0xfc00 {
bail!(
"OAuth endpoint must not point to IPv6 unique-local address: {}",
ip
);
}
}
url::Host::Domain(_) => {
// Domain-based SSRF prevention requires resolver-level checks.
// See known limitation in the doc comment above.
}
}
}
Ok(())
}
/// Parsed from the MCP server's WWW-Authenticate header or well-known endpoint
/// per RFC 9728 (OAuth 2.0 Protected Resource Metadata).
#[derive(Debug, Clone, Serialize, Deserialize)]View on GitHub (pinned to f4178619ac)
Solutions
- Advertise a global (public) IPv6 address or, better, a DNS name for the endpoint
- Give the internal service a domain name resolvable by the client and put that name in the metadata document
- For same-machine testing stick to http://localhost or http://127.0.0.1, which the earlier checks allow
Example fix
// before "token_endpoint": "https://[fdab:dead:beef::1]/token" // after "token_endpoint": "https://auth.internal.example.com/token"
Defensive patterns
Strategy: validation
Validate before calling
use url::Url;
fn is_ula_ipv6(url: &Url) -> bool {
matches!(url.host(), Some(url::Host::Ipv6(ip))
if (ip.segments()[0] & 0xfe00) == 0xfc00)
}
if is_ula_ipv6(&endpoint) {
bail_user_config!("endpoint uses fc00::/7 unique-local address: {}", endpoint);
} Type guard
fn is_global_ipv6_literal(url: &Url) -> Option<bool> {
match url.host() {
Some(url::Host::Ipv6(ip)) => Some((ip.segments()[0] & 0xfe00) != 0xfc00),
_ => None,
}
} Try / catch
match validate_oauth_url(&endpoint) {
Err(err) if err.to_string().contains("unique-local") => {
// ULA literal in metadata — swap to a DNS name or global address
report_metadata_bug(&endpoint, err);
Err(err)
}
other => other,
} Prevention
- Docker/IPv6 pools default to fd00::/8 ULAs — never paste container IPs into OAuth metadata
- If you run internal IPv6, maintain split-DNS names for services instead of ULA literals
- Remember the client blocks IP literals only; domain-to-ULA resolution is a documented gap
When it happens
Trigger: An endpoint URL whose host is an IPv6 literal whose first 7 bits are 1111110 — e.g. https://[fd12:3456:789a::1]/token or anything in fc00::/7 — reaching the manual prefix check after passing the mapped-v4, unspecified, and multicast checks.
Common situations: Internal IPv6-only networks (docker ipv6 ULA subnets default to fd00::/64-based pools) whose services advertise ULA literals in OAuth metadata; tailnet/mesh VPNs that hand out fdxx addresses; someone attempting the IPv6 form of a private-range SSRF.
Related errors
- OAuth endpoint must not point to private/reserved IP: ::ffff
- OAuth endpoint must not point to reserved IPv6 address: {}
- OAuth endpoint must not point to private/reserved IP: {}
- OAuth endpoint must use HTTPS (got {}://{})
- WWW-Authenticate header does not use Bearer scheme
AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20).
Data as JSON: /api/errors/8383f06f0a0425d9.
Report an issue: GitHub.