{"record":{"id":"3d93d0881743dccf","repo":"zed-industries/zed","slug":"oauth-endpoint-must-not-point-to-private-reserved","errorCode":null,"errorMessage":"OAuth endpoint must not point to private/reserved IP: {}","messagePattern":"OAuth endpoint must not point to private/reserved IP: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/context_server/src/oauth.rs","lineNumber":84,"sourceCode":"/// protections against private/reserved IP ranges.\n///\n/// This wraps [`require_https_or_loopback`] and adds IP-range checks to prevent\n/// an attacker-controlled MCP server from directing Zed to fetch internal\n/// network resources via metadata URLs.\n///\n/// **Known limitation:** Domain-name URLs that resolve to private IPs are *not*\n/// blocked here — full mitigation requires resolver-level validation (e.g. a\n/// custom `Resolve` implementation). This function only blocks IP-literal URLs.\nfn validate_oauth_url(url: &Url) -> Result<()> {\n    require_https_or_loopback(url)?;\n\n    if let Some(host) = url.host() {\n        match host {\n            url::Host::Ipv4(ip) => {\n                // Loopback is already allowed by require_https_or_loopback.\n                if ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified()\n                {\n                    bail!(\n                        \"OAuth endpoint must not point to private/reserved IP: {}\",\n                        ip\n                    );\n                }\n            }\n            url::Host::Ipv6(ip) => {\n                // Check for IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) which\n                // could bypass the IPv4 checks above.\n                if let Some(mapped_v4) = ip.to_ipv4_mapped() {\n                    if mapped_v4.is_private()\n                        || mapped_v4.is_link_local()\n                        || mapped_v4.is_broadcast()\n                        || mapped_v4.is_unspecified()\n                    {\n                        bail!(\n                            \"OAuth endpoint must not point to private/reserved IP: ::ffff:{}\",\n                            mapped_v4\n                        );","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/zed-industries/zed/blob/f4178619acd0d47ea1f76a2025c42962c6d6638c/crates/context_server/src/oauth.rs#L66-L102","documentation":"The SSRF guard in validate_oauth_url rejects OAuth endpoint URLs whose host is an IP-literal in a private or reserved IPv4 range: RFC 1918 private space (10/8, 172.16/12, 192.168/16), link-local 169.254/16 (including cloud metadata addresses), broadcast 255.255.255.255, and unspecified 0.0.0.0. Loopback was already permitted by require_https_or_loopback, so seeing this message means a non-loopback internal address was reached. The goal is to stop a malicious MCP server from directing Zed's HTTP client at internal network resources such as cloud instance-metadata endpoints.","triggerScenarios":"validate_oauth_url() receives an endpoint URL with an IPv4 literal host that satisfies ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified() — e.g. http://169.254.169.254/latest/meta-data or https://10.1.2.3/token (note https does not bypass this check; only the value of the host matters).","commonSituations":"An MCP server under test runs inside a docker/k8s cluster and advertises its cluster-internal IP (10.x or 172.17.x) in authorization_servers or token_endpoint; an attacker-controlled server returns resource_metadata pointing at 169.254.169.254 to harvest cloud credentials; home-lab setups that legitimately use 192.168.x addresses for OAuth and get blocked by the guard.","solutions":["Give the OAuth endpoint a public DNS name reachable over HTTPS instead of a raw private IP","If the endpoint is on the same machine, use localhost/127.0.0.1 which the earlier require_https_or_loopback check already allows","Expose the internal service through an ingress/proxy with a public hostname so the advertised URL contains a domain, not the private IP","If you are auditing a security report, treat the trigger URL as malicious — this bail is the SSRF mitigation working as designed"],"exampleFix":"// before\n\"token_endpoint\": \"https://10.0.0.8:8443/token\"\n\n// after\n\"token_endpoint\": \"https://auth.internal.example.com:8443/token\"","handlingStrategy":"validation","validationCode":"use url::Url;\n\nfn ipv4_host_allowed(url: &Url) -> bool {\n    match url.host() {\n        Some(url::Host::Ipv4(ip)) => {\n            !(ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified())\n        }\n        _ => true, // domains/IPv6 handled by their own checks\n    }\n}\n\nlet endpoint = Url::parse(&server_advertised)?;\nif !ipv4_host_allowed(&endpoint) {\n    return bail_user_config(\"endpoint uses private/reserved IPv4 literal: {}\", endpoint);\n}","typeGuard":"fn is_public_ipv4_literal(url: &Url) -> Option<bool> {\n    match url.host() {\n        Some(url::Host::Ipv4(ip)) => Some(\n            !(ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified()),\n        ),\n        _ => None, // not an IPv4 literal\n    }\n}","tryCatchPattern":"match validate_oauth_url(&endpoint) {\n    Err(err) if err.to_string().contains(\"private/reserved IP\") => {\n        // log as potential SSRF attempt / fix server metadata to use a domain name\n        log::warn!(\"blocked internal-IP OAuth endpoint: {endpoint}\");\n        return Err(err);\n    }\n    other => other,\n}","preventionTips":["Never advertise raw internal IPs in OAuth metadata; use DNS names","Remember domains resolving to private IPs are NOT caught (documented limitation) — pair with egress controls if that matters to you","When self-hosting for LAN use, put the service behind a local DNS name and be aware IP-literal forms are blocked by design"],"tags":["oauth","mcp","ssrf","ipv4","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-14T05:17:10.506Z"}