{"record":{"id":"5c85a2a70688de1a","repo":"google-gemini/gemini-cli","slug":"oauth-endpoint-origin-parsed-origin-does-not","errorCode":null,"errorMessage":"OAuth endpoint origin \"${parsed.origin}\" does not match expected origin \"${expected}\".","messagePattern":"OAuth endpoint origin \"(.+?)\" does not match expected origin \"(.+?)\"\\.","errorType":"exception","errorClass":"OAuthSecurityError","httpStatus":null,"severity":"error","filePath":"packages/core/src/mcp/oauth-utils.ts","lineNumber":113,"sourceCode":"  const isLoopback = isLoopbackHost(hostname);\n\n  if (isHttp && (!options?.allowLoopback || !isLoopback)) {\n    throw new OAuthSecurityError(\n      `Insecure HTTP OAuth endpoint \"${resolvedUrl}\" is not allowed. OAuth endpoints must use HTTPS unless connecting to localhost.`,\n    );\n  }\n\n  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.`,","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/3c311beac2e78336816dd4a123db39743f9fbf85/packages/core/src/mcp/oauth-utils.ts#L95-L131","documentation":"The endpoint URL parses fine, but its origin (scheme + host + port) does not match the expectedOrigin the caller pinned. This is an anti-SSRF/redirect-attack check: OAuth metadata discovered from a server must not point the client at a different origin than the one the operator explicitly trusted.","triggerScenarios":"Calling validateOAuthEndpointUrl('https://evil.example.com/authorize', { expectedOrigin: 'https://auth.example.com' }); also legitimate mismatches like a port difference (https://host:8443 vs https://host) or scheme difference (http vs https).","commonSituations":"Compromised or misconfigured server advertising OAuth endpoints on a different domain than its own; CDN/proxy setups where metadata references an internal host; ports omitted or added between environments; http-vs-https drift between config and discovered metadata.","solutions":["Confirm the endpoint actually should be on the pinned origin; if the metadata legitimately points elsewhere (dedicated identity provider), update expectedOrigin to that trusted origin","Fix scheme/port mismatches: URL.origin includes the port when non-default, so 'https://host:8443' ≠ 'https://host'","If the mismatch is unexpected, treat it as a security signal — verify the server's OAuth metadata has not been tampered with before overriding anything","Ensure the value used for expectedOrigin is derived from the same source (e.g. the MCP server URL) you intend to trust"],"exampleFix":"// before\nawait validateOAuthEndpointUrl(metadataEndpoint, { expectedOrigin: 'https://api.example.com' });\n// metadataEndpoint = 'https://auth.example.com/authorize' -> mismatch\n\n// after\nawait validateOAuthEndpointUrl(metadataEndpoint, { expectedOrigin: 'https://auth.example.com' });","handlingStrategy":"try-catch","validationCode":"function originsMatch(endpoint: string, expectedOrigin: string): boolean | null {\n  try {\n    return new URL(endpoint).origin === new URL(expectedOrigin).origin;\n  } catch { return null; }\n}\n\nconst ok = originsMatch(endpoint, expectedOrigin);\nif (ok === false) console.warn('Endpoint origin differs from pinned origin — verify before proceeding');","typeGuard":"function matchesOrigin(endpoint: string, origin: string): endpoint is string {\n  try { return new URL(endpoint).origin === new URL(origin).origin; } catch { return false; }\n}","tryCatchPattern":"try {\n  await validateOAuthEndpointUrl(url, { expectedOrigin });\n} catch (e) {\n  if (e instanceof OAuthSecurityError && e.message.includes('does not match expected origin')) {\n    // decide deliberately: update the pinned origin (if the new host is trusted) or abort (possible SSRF/redirect attack)\n  }\n  throw e;\n}","preventionTips":["Derive expectedOrigin from the same trusted source as the server URL rather than hardcoding a second value","Remember URL.origin includes non-default ports — include them in the pinned origin","Alert on unexpected origin mismatches; they can indicate compromised metadata"],"tags":["oauth","origin-pinning","ssrf-protection","security"],"backgroundTag":"origin-mismatch","analyzedSha":"3c311beac2e78336816dd4a123db39743f9fbf85","analyzedAt":"2026-08-27T19:07:12.298Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}