{"record":{"id":"202a3caea39605e9","repo":"google-gemini/gemini-cli","slug":"oauth-endpoint-resolvedurl-points-to-private","errorCode":null,"errorMessage":"OAuth endpoint \"${resolvedUrl}\" points to private or reserved IP address which is blocked.","messagePattern":"OAuth endpoint \"(.+?)\" points to private or reserved IP address which is blocked\\.","errorType":"exception","errorClass":"OAuthSecurityError","httpStatus":null,"severity":"error","filePath":"packages/core/src/mcp/oauth-utils.ts","lineNumber":130,"sourceCode":"    if (parsed.origin !== expected) {\n      throw new OAuthSecurityError(\n        `OAuth endpoint origin \"${parsed.origin}\" does not match expected origin \"${expected}\".`,\n      );\n    }\n  }\n\n  if (isLoopback) {\n    if (!options?.allowLoopback) {\n      throw new OAuthSecurityError(\n        `Loopback OAuth endpoint \"${resolvedUrl}\" is not allowed for remote MCP servers.`,\n      );\n    }\n    return parsed.toString();\n  }\n\n  // Non-loopback host: check literal IP\n  if (isAddressPrivate(hostname)) {\n    throw new OAuthSecurityError(\n      `OAuth endpoint \"${resolvedUrl}\" points to private or reserved IP address which is blocked.`,\n    );\n  }\n\n  // Asynchronous DNS resolution to prevent DNS rebinding / SSRF\n  try {\n    const addresses = await lookup(hostname, { all: true });\n    if (!addresses || addresses.length === 0) {\n      throw new OAuthSecurityError(\n        `Failed to resolve hostname \"${hostname}\" for OAuth endpoint \"${resolvedUrl}\".`,\n      );\n    }\n\n    for (const addr of addresses) {\n      if (isAddressPrivate(addr.address)) {\n        throw new OAuthSecurityError(\n          `OAuth endpoint \"${resolvedUrl}\" resolves to private network address \"${addr.address}\" which is blocked.`,\n        );","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/3c311beac2e78336816dd4a123db39743f9fbf85/packages/core/src/mcp/oauth-utils.ts#L112-L148","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use a public, resolvable hostname for the OAuth endpoint and expose it via TLS (the intended production shape)","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","For containerized setups, route through an ingress/tunnel (e.g. TLS-terminated proxy with a public hostname) rather than the internal IP","If a hostname is used instead of a literal IP, see error 9 for the DNS-based equivalent"],"exampleFix":"// before\nawait validateOAuthEndpointUrl('https://192.168.1.10/oauth/authorize');\n\n// after\nawait validateOAuthEndpointUrl('https://auth.internal.example.com/oauth/authorize');","handlingStrategy":"validation","validationCode":"import { isIP } from 'node:net';\n\nfunction isPrivateLiteralIp(v: string): boolean {\n  try {\n    const h = new URL(v).hostname.replace(/^\\[|\\]$/g, '');\n    if (isIP(h) === 0) return false; // hostname, not literal IP\n    return (\n      h.startsWith('10.') || h.startsWith('192.168.') ||\n      /^172\\.(1[6-9]|2\\d|3[01])\\./.test(h) || h.startsWith('169.254.') ||\n      h === '0.0.0.0' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')\n    );\n  } catch { return false; }\n}\n\nif (isPrivateLiteralIp(endpoint)) throw new Error('Refusing private-IP OAuth endpoint');","typeGuard":"function isPublicHostUrl(v: string): boolean {\n  try { const h = new URL(v).hostname; return !isPrivateLiteralIp(v) && isIP(h) !== 0 ? true : !isPrivateLiteralIp(v); } catch { return false; }\n}","tryCatchPattern":"try {\n  await validateOAuthEndpointUrl(endpoint);\n} catch (e) {\n  if (e instanceof OAuthSecurityError && e.message.includes('private or reserved IP address')) {\n    // raw private IPs are always blocked; switch to a hostname with a public DNS record / public ingress\n  }\n  throw e;\n}","preventionTips":["Never configure OAuth endpoints as raw private IPs; use DNS names","For internal deployments, expose the auth server through a TLS ingress with a public hostname","Add a config lint that rejects URLs whose hostname is a private-range literal IP"],"tags":["oauth","ssrf-protection","private-ip","network-security"],"backgroundTag":"private-ip-blocked","analyzedSha":"3c311beac2e78336816dd4a123db39743f9fbf85","analyzedAt":"2026-08-27T19:07:12.298Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}