{"record":{"id":"8383f06f0a0425d9","repo":"zed-industries/zed","slug":"oauth-endpoint-must-not-point-to-ipv6-unique-local","errorCode":null,"errorMessage":"OAuth endpoint must not point to IPv6 unique-local address: {}","messagePattern":"OAuth endpoint must not point to IPv6 unique-local address: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/context_server/src/oauth.rs","lineNumber":115,"sourceCode":"                        || mapped_v4.is_unspecified()\n                    {\n                        bail!(\n                            \"OAuth endpoint must not point to private/reserved IP: ::ffff:{}\",\n                            mapped_v4\n                        );\n                    }\n                }\n\n                if ip.is_unspecified() || ip.is_multicast() {\n                    bail!(\n                        \"OAuth endpoint must not point to reserved IPv6 address: {}\",\n                        ip\n                    );\n                }\n                // IPv6 Unique Local Addresses (fc00::/7). is_unique_local() is\n                // nightly-only, so check the prefix manually.\n                if (ip.segments()[0] & 0xfe00) == 0xfc00 {\n                    bail!(\n                        \"OAuth endpoint must not point to IPv6 unique-local address: {}\",\n                        ip\n                    );\n                }\n            }\n            url::Host::Domain(_) => {\n                // Domain-based SSRF prevention requires resolver-level checks.\n                // See known limitation in the doc comment above.\n            }\n        }\n    }\n\n    Ok(())\n}\n\n/// Parsed from the MCP server's WWW-Authenticate header or well-known endpoint\n/// per RFC 9728 (OAuth 2.0 Protected Resource Metadata).\n#[derive(Debug, Clone, Serialize, Deserialize)]","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/zed-industries/zed/blob/f4178619acd0d47ea1f76a2025c42962c6d6638c/crates/context_server/src/oauth.rs#L97-L133","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n\"token_endpoint\": \"https://[fdab:dead:beef::1]/token\"\n\n// after\n\"token_endpoint\": \"https://auth.internal.example.com/token\"","handlingStrategy":"validation","validationCode":"use url::Url;\n\nfn is_ula_ipv6(url: &Url) -> bool {\n    matches!(url.host(), Some(url::Host::Ipv6(ip))\n        if (ip.segments()[0] & 0xfe00) == 0xfc00)\n}\n\nif is_ula_ipv6(&endpoint) {\n    bail_user_config!(\"endpoint uses fc00::/7 unique-local address: {}\", endpoint);\n}","typeGuard":"fn is_global_ipv6_literal(url: &Url) -> Option<bool> {\n    match url.host() {\n        Some(url::Host::Ipv6(ip)) => Some((ip.segments()[0] & 0xfe00) != 0xfc00),\n        _ => None,\n    }\n}","tryCatchPattern":"match validate_oauth_url(&endpoint) {\n    Err(err) if err.to_string().contains(\"unique-local\") => {\n        // ULA literal in metadata — swap to a DNS name or global address\n        report_metadata_bug(&endpoint, err);\n        Err(err)\n    }\n    other => other,\n}","preventionTips":["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"],"tags":["oauth","mcp","ssrf","ipv6","url-validation","security"],"backgroundTag":"ssrf-private-ip-blocked","analyzedSha":"f4178619acd0d47ea1f76a2025c42962c6d6638c","analyzedAt":"2026-08-20T19:29:52.058Z","contentChangedAt":"2026-08-20T19:29:52.058Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}