google-gemini/gemini-cli · error · OAuthSecurityError

OAuth endpoint "${resolvedUrl}" points to private or reserve

Error message

OAuth endpoint "${resolvedUrl}" points to private or reserved IP address which is blocked.

What it means

The non-loopback endpoint hostname is a literal IP address in a private or reserved range (e.g. 10.x, 192.168.x, 172.16-31.x, 169.254.x, 0.0.0.0, or equivalent IPv6). Directing OAuth flows at private networks enables SSRF attacks, so literal private IPs are rejected before DNS is even consulted.

Source

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

    if (parsed.origin !== expected) {
      throw new OAuthSecurityError(
        `OAuth endpoint origin "${parsed.origin}" does not match expected origin "${expected}".`,
      );
    }
  }

  if (isLoopback) {
    if (!options?.allowLoopback) {
      throw new OAuthSecurityError(
        `Loopback OAuth endpoint "${resolvedUrl}" is not allowed for remote MCP servers.`,
      );
    }
    return parsed.toString();
  }

  // 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.`,
        );

View on GitHub (pinned to 3c311beac2)

Solutions

  1. Use a public, resolvable hostname for the OAuth endpoint and expose it via TLS (the intended production shape)
  2. If this is genuinely an internal deployment, put a DNS name on the service and ensure it resolves to a public address, or run the validation with a hostname whose DNS points where intended — note literal private IPs are unconditionally blocked, so the fix is to not use a raw private IP in the URL
  3. For containerized setups, route through an ingress/tunnel (e.g. TLS-terminated proxy with a public hostname) rather than the internal IP
  4. If a hostname is used instead of a literal IP, see error 9 for the DNS-based equivalent

Example fix

// before
await validateOAuthEndpointUrl('https://192.168.1.10/oauth/authorize');

// after
await validateOAuthEndpointUrl('https://auth.internal.example.com/oauth/authorize');
Defensive patterns

Strategy: validation

Validate before calling

import { isIP } from 'node:net';

function isPrivateLiteralIp(v: string): boolean {
  try {
    const h = new URL(v).hostname.replace(/^\[|\]$/g, '');
    if (isIP(h) === 0) return false; // hostname, not literal IP
    return (
      h.startsWith('10.') || h.startsWith('192.168.') ||
      /^172\.(1[6-9]|2\d|3[01])\./.test(h) || h.startsWith('169.254.') ||
      h === '0.0.0.0' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')
    );
  } catch { return false; }
}

if (isPrivateLiteralIp(endpoint)) throw new Error('Refusing private-IP OAuth endpoint');

Type guard

function isPublicHostUrl(v: string): boolean {
  try { const h = new URL(v).hostname; return !isPrivateLiteralIp(v) && isIP(h) !== 0 ? true : !isPrivateLiteralIp(v); } catch { return false; }
}

Try / catch

try {
  await validateOAuthEndpointUrl(endpoint);
} catch (e) {
  if (e instanceof OAuthSecurityError && e.message.includes('private or reserved IP address')) {
    // raw private IPs are always blocked; switch to a hostname with a public DNS record / public ingress
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing 'https://192.168.1.10/authorize', 'https://10.0.0.5/token', or 'https://[fe80::1]/authorize' (non-loopback private/reserved literal IPs) to validateOAuthEndpointUrl.

Common situations: Internal/on-prem deployments addressed by raw private IPs; Docker/Kubernetes setups where the discovered endpoint is an internal service IP (e.g. 172.x docker bridge range); home-lab servers referenced by LAN IP; metadata endpoints accidentally returning internal addresses.

Related errors


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