google-gemini/gemini-cli · error · OAuthSecurityError

OAuth endpoint "${resolvedUrl}" resolves to private network

Error message

OAuth endpoint "${resolvedUrl}" resolves to private network address "${addr.address}" which is blocked.

What it means

DNS resolution succeeded, but at least one resolved A/AAAA address is private or reserved. This closes the DNS-rebinding/SSRF gap: even if the URL uses a public-looking hostname, the library checks every address it actually resolves to and blocks the request if any land in private/reserved ranges.

Source

Thrown at packages/core/src/mcp/oauth-utils.ts:146

  // Non-loopback host: check literal IP
  if (isAddressPrivate(hostname)) {
    throw new OAuthSecurityError(
      `OAuth endpoint "${resolvedUrl}" points to private or reserved IP address which is blocked.`,
    );
  }

  // Asynchronous DNS resolution to prevent DNS rebinding / SSRF
  try {
    const addresses = await lookup(hostname, { all: true });
    if (!addresses || addresses.length === 0) {
      throw new OAuthSecurityError(
        `Failed to resolve hostname "${hostname}" for OAuth endpoint "${resolvedUrl}".`,
      );
    }

    for (const addr of addresses) {
      if (isAddressPrivate(addr.address)) {
        throw new OAuthSecurityError(
          `OAuth endpoint "${resolvedUrl}" resolves to private network address "${addr.address}" which is blocked.`,
        );
      }
    }
  } catch (error) {
    if (error instanceof OAuthSecurityError) {
      throw error;
    }
    throw new OAuthSecurityError(
      `DNS lookup failed for OAuth endpoint host "${hostname}": ${getErrorMessage(error)}`,
    );
  }

  return parsed.toString();
}

/**
 * OAuth authorization server metadata as per RFC 8414.

View on GitHub (pinned to 3c311beac2)

Solutions

  1. Check what the hostname actually resolves to in this environment (nslookup/dig, or dns.lookup(host, { all: true })) and remove/replace private-record mappings (hosts-file entries, split-horizon overrides)
  2. For local testing, use a real loopback URL with { allowLoopback: true } instead of mapping a fake public hostname to a private IP
  3. If split-horizon DNS is legitimate for your deployment, run the client in a context where the name resolves publicly, or route via a TLS-terminated public ingress
  4. Treat an unexpected private resolution as a possible DNS-rebinding signal and verify DNS integrity before working around it

Example fix

# before (/etc/hosts)
127.0.0.1 auth.example.com
const url = await validateOAuthEndpointUrl('https://auth.example.com/authorize'); // throws

# after: remove the hosts override and use the real service, or explicitly:
const url = await validateOAuthEndpointUrl('http://localhost:3000/authorize', { allowLoopback: true });
Defensive patterns

Strategy: validation

Validate before calling

import { lookup } from 'node:dns/promises';

function isPrivateIp(a: string): boolean {
  return a.startsWith('10.') || a.startsWith('192.168.') || /^172\.(1[6-9]|2\d|3[01])\./.test(a) ||
    a.startsWith('169.254.') || a === '127.0.0.1' || a === '::1' || a.startsWith('fe80:') || a.startsWith('fc') || a.startsWith('fd');
}

async function resolvesOnlyPublicly(host: string): Promise<boolean> {
  const addrs = await lookup(host, { all: true });
  return addrs.length > 0 && addrs.every((a) => !isPrivateIp(a.address));
}

if (!(await resolvesOnlyPublicly(new URL(endpoint).hostname))) {
  throw new Error('Endpoint resolves to private addresses (SSRF guard would reject it)');
}

Type guard

async function resolvesToPublicOnly(host: string): Promise<boolean> {
  try { return (await lookup(host, { all: true })).every((a) => !isPrivateIp(a.address)); } catch { return false; }
}

Try / catch

try {
  await validateOAuthEndpointUrl(endpoint);
} catch (e) {
  if (e instanceof OAuthSecurityError && e.message.includes('resolves to private network address')) {
    // check dig/nslookup from this host; remove hosts-file or split-horizon overrides mapping the name to private IPs
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a public hostname whose DNS points at a private IP — e.g. a hosts-file entry mapping auth.example.com to 127.0.0.1 or 192.168.x.x, split-horizon DNS returning internal addresses, or a load-balancer record resolving to an internal range (the lookup uses all: true, so one bad record among many triggers the block).

Common situations: Corporate split-horizon DNS where the same name resolves internally to a 10.x/192.168.x address; /etc/hosts overrides left over from local testing; DNS rebinding attack attempts; testing with public-looking hostnames that map to local/docker IPs.

Related errors


AI-assisted analysis of google-gemini/gemini-cli@3c311beac2 (2026-08-27). Data as JSON: /api/errors/e7133ee03a13438d. Report an issue: GitHub.