google-gemini/gemini-cli · error · OAuthSecurityError

Loopback OAuth endpoint "${resolvedUrl}" is not allowed for

Error message

Loopback OAuth endpoint "${resolvedUrl}" is not allowed for remote MCP servers.

What it means

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.

Source

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

  if (options?.expectedOrigin) {
    let expected: string;
    try {
      expected = new URL(options.expectedOrigin).origin;
    } catch {
      throw new OAuthSecurityError(
        `Invalid expected origin "${options.expectedOrigin}".`,
      );
    }
    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(

View on GitHub (pinned to 3c311beac2)

Solutions

  1. Pass { allowLoopback: true } when validating local development endpoints
  2. For anything other than local dev, point the endpoint at the real remote host instead of localhost
  3. In tests, ensure the test harness/config enables allowLoopback for local mock servers

Example fix

// before
await validateOAuthEndpointUrl('http://localhost:3000/authorize');

// after
await validateOAuthEndpointUrl('http://localhost:3000/authorize', { allowLoopback: true });
Defensive patterns

Strategy: validation

Validate before calling

const isLocalDev = process.env.NODE_ENV !== 'production';
const isLoopback = (() => { try { const h = new URL(endpoint).hostname; return h === 'localhost' || h.startsWith('127.') || h === '[::1]' || h === '::1'; } catch { return false; } })();

const opts = { allowLoopback: isLocalDev && isLoopback };
await validateOAuthEndpointUrl(endpoint, opts);

Type guard

function isLoopbackEndpoint(v: string): boolean {
  try { const h = new URL(v).hostname; return h === 'localhost' || h.startsWith('127.') || h === '::1'; } catch { return false; }
}

Try / catch

try {
  await validateOAuthEndpointUrl(endpoint);
} catch (e) {
  if (e instanceof OAuthSecurityError && e.message.includes('not allowed for remote MCP servers')) {
    // localhost endpoint: retry with { allowLoopback: true } in dev, or use the real remote host in prod
  }
  throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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