zed-industries/zed · error

OAuth endpoint must not point to private/reserved IP: {}

Error message

OAuth endpoint must not point to private/reserved IP: {}

What it means

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.

Source

Thrown at crates/context_server/src/oauth.rs:84

/// protections against private/reserved IP ranges.
///
/// This wraps [`require_https_or_loopback`] and adds IP-range checks to prevent
/// an attacker-controlled MCP server from directing Zed to fetch internal
/// network resources via metadata URLs.
///
/// **Known limitation:** Domain-name URLs that resolve to private IPs are *not*
/// blocked here — full mitigation requires resolver-level validation (e.g. a
/// custom `Resolve` implementation). This function only blocks IP-literal URLs.
fn validate_oauth_url(url: &Url) -> Result<()> {
    require_https_or_loopback(url)?;

    if let Some(host) = url.host() {
        match host {
            url::Host::Ipv4(ip) => {
                // Loopback is already allowed by require_https_or_loopback.
                if ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified()
                {
                    bail!(
                        "OAuth endpoint must not point to private/reserved IP: {}",
                        ip
                    );
                }
            }
            url::Host::Ipv6(ip) => {
                // Check for IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) which
                // could bypass the IPv4 checks above.
                if let Some(mapped_v4) = ip.to_ipv4_mapped() {
                    if mapped_v4.is_private()
                        || mapped_v4.is_link_local()
                        || mapped_v4.is_broadcast()
                        || mapped_v4.is_unspecified()
                    {
                        bail!(
                            "OAuth endpoint must not point to private/reserved IP: ::ffff:{}",
                            mapped_v4
                        );

View on GitHub (pinned to f4178619ac)

Solutions

  1. Give the OAuth endpoint a public DNS name reachable over HTTPS instead of a raw private IP
  2. If the endpoint is on the same machine, use localhost/127.0.0.1 which the earlier require_https_or_loopback check already allows
  3. Expose the internal service through an ingress/proxy with a public hostname so the advertised URL contains a domain, not the private IP
  4. If you are auditing a security report, treat the trigger URL as malicious — this bail is the SSRF mitigation working as designed

Example fix

// before
"token_endpoint": "https://10.0.0.8:8443/token"

// after
"token_endpoint": "https://auth.internal.example.com:8443/token"
Defensive patterns

Strategy: validation

Validate before calling

use url::Url;

fn ipv4_host_allowed(url: &Url) -> bool {
    match url.host() {
        Some(url::Host::Ipv4(ip)) => {
            !(ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified())
        }
        _ => true, // domains/IPv6 handled by their own checks
    }
}

let endpoint = Url::parse(&server_advertised)?;
if !ipv4_host_allowed(&endpoint) {
    return bail_user_config("endpoint uses private/reserved IPv4 literal: {}", endpoint);
}

Type guard

fn is_public_ipv4_literal(url: &Url) -> Option<bool> {
    match url.host() {
        Some(url::Host::Ipv4(ip)) => Some(
            !(ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified()),
        ),
        _ => None, // not an IPv4 literal
    }
}

Try / catch

match validate_oauth_url(&endpoint) {
    Err(err) if err.to_string().contains("private/reserved IP") => {
        // log as potential SSRF attempt / fix server metadata to use a domain name
        log::warn!("blocked internal-IP OAuth endpoint: {endpoint}");
        return Err(err);
    }
    other => other,
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20). Data as JSON: /api/errors/3d93d0881743dccf. Report an issue: GitHub.