{"record":{"id":"157585c6018a7eb6","repo":"google-gemini/gemini-cli","slug":"loopback-oauth-endpoint-resolvedurl-is-not-al","errorCode":null,"errorMessage":"Loopback OAuth endpoint \"${resolvedUrl}\" is not allowed for remote MCP servers.","messagePattern":"Loopback OAuth endpoint \"(.+?)\" is not allowed for remote MCP servers\\.","errorType":"exception","errorClass":"OAuthSecurityError","httpStatus":null,"severity":"error","filePath":"packages/core/src/mcp/oauth-utils.ts","lineNumber":121,"sourceCode":"  if (options?.expectedOrigin) {\n    let expected: string;\n    try {\n      expected = new URL(options.expectedOrigin).origin;\n    } catch {\n      throw new OAuthSecurityError(\n        `Invalid expected origin \"${options.expectedOrigin}\".`,\n      );\n    }\n    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(","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/3c311beac2e78336816dd4a123db39743f9fbf85/packages/core/src/mcp/oauth-utils.ts#L103-L139","documentation":"The endpoint host is a loopback address (localhost / 127.x / ::1) but the caller did not pass { allowLoopback: true }. Remote MCP OAuth endpoints are never expected to live on the client's own machine, so loopback endpoints are blocked by default — this blocks SSRF-style attacks that redirect OAuth flows to local services.","triggerScenarios":"Passing 'http://localhost:3000/authorize' or 'https://127.0.0.1:8443/token' to validateOAuthEndpointUrl without allowLoopback: true (note: loopback check happens before the HTTP check, so even https loopback hits this).","commonSituations":"Local development against a locally hosted auth server where the caller forgot the allowLoopback option; integration tests pointing at a local mock OAuth server; discovered metadata from a local server being validated with remote-server settings.","solutions":["Pass { allowLoopback: true } when validating local development endpoints","For anything other than local dev, point the endpoint at the real remote host instead of localhost","In tests, ensure the test harness/config enables allowLoopback for local mock servers"],"exampleFix":"// before\nawait validateOAuthEndpointUrl('http://localhost:3000/authorize');\n\n// after\nawait validateOAuthEndpointUrl('http://localhost:3000/authorize', { allowLoopback: true });","handlingStrategy":"validation","validationCode":"const isLocalDev = process.env.NODE_ENV !== 'production';\nconst isLoopback = (() => { try { const h = new URL(endpoint).hostname; return h === 'localhost' || h.startsWith('127.') || h === '[::1]' || h === '::1'; } catch { return false; } })();\n\nconst opts = { allowLoopback: isLocalDev && isLoopback };\nawait validateOAuthEndpointUrl(endpoint, opts);","typeGuard":"function isLoopbackEndpoint(v: string): boolean {\n  try { const h = new URL(v).hostname; return h === 'localhost' || h.startsWith('127.') || h === '::1'; } catch { return false; }\n}","tryCatchPattern":"try {\n  await validateOAuthEndpointUrl(endpoint);\n} catch (e) {\n  if (e instanceof OAuthSecurityError && e.message.includes('not allowed for remote MCP servers')) {\n    // localhost endpoint: retry with { allowLoopback: true } in dev, or use the real remote host in prod\n  }\n  throw e;\n}","preventionTips":["Gate allowLoopback behind an explicit isLocalDev flag so production can never enable it","Use localhost URLs only for local mock auth servers in tests","For production, always validate against the server's real public hostname"],"tags":["oauth","loopback","ssrf-protection","local-development"],"backgroundTag":"loopback-endpoint-blocked","analyzedSha":"3c311beac2e78336816dd4a123db39743f9fbf85","analyzedAt":"2026-08-27T19:07:12.298Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}