google-gemini/gemini-cli · error · OAuthSecurityError
OAuth endpoint "${resolvedUrl}" resolves to private network
Error message
OAuth endpoint "${resolvedUrl}" resolves to private network address "${addr.address}" which is blocked. What it means
DNS resolution succeeded, but at least one resolved A/AAAA address is private or reserved. This closes the DNS-rebinding/SSRF gap: even if the URL uses a public-looking hostname, the library checks every address it actually resolves to and blocks the request if any land in private/reserved ranges.
Source
Thrown at packages/core/src/mcp/oauth-utils.ts:146
// 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.`,
);
}
}
} catch (error) {
if (error instanceof OAuthSecurityError) {
throw error;
}
throw new OAuthSecurityError(
`DNS lookup failed for OAuth endpoint host "${hostname}": ${getErrorMessage(error)}`,
);
}
return parsed.toString();
}
/**
* OAuth authorization server metadata as per RFC 8414.View on GitHub (pinned to 3c311beac2)
Solutions
- Check what the hostname actually resolves to in this environment (nslookup/dig, or dns.lookup(host, { all: true })) and remove/replace private-record mappings (hosts-file entries, split-horizon overrides)
- For local testing, use a real loopback URL with { allowLoopback: true } instead of mapping a fake public hostname to a private IP
- If split-horizon DNS is legitimate for your deployment, run the client in a context where the name resolves publicly, or route via a TLS-terminated public ingress
- Treat an unexpected private resolution as a possible DNS-rebinding signal and verify DNS integrity before working around it
Example fix
# before (/etc/hosts)
127.0.0.1 auth.example.com
const url = await validateOAuthEndpointUrl('https://auth.example.com/authorize'); // throws
# after: remove the hosts override and use the real service, or explicitly:
const url = await validateOAuthEndpointUrl('http://localhost:3000/authorize', { allowLoopback: true }); Defensive patterns
Strategy: validation
Validate before calling
import { lookup } from 'node:dns/promises';
function isPrivateIp(a: string): boolean {
return a.startsWith('10.') || a.startsWith('192.168.') || /^172\.(1[6-9]|2\d|3[01])\./.test(a) ||
a.startsWith('169.254.') || a === '127.0.0.1' || a === '::1' || a.startsWith('fe80:') || a.startsWith('fc') || a.startsWith('fd');
}
async function resolvesOnlyPublicly(host: string): Promise<boolean> {
const addrs = await lookup(host, { all: true });
return addrs.length > 0 && addrs.every((a) => !isPrivateIp(a.address));
}
if (!(await resolvesOnlyPublicly(new URL(endpoint).hostname))) {
throw new Error('Endpoint resolves to private addresses (SSRF guard would reject it)');
} Type guard
async function resolvesToPublicOnly(host: string): Promise<boolean> {
try { return (await lookup(host, { all: true })).every((a) => !isPrivateIp(a.address)); } catch { return false; }
} Try / catch
try {
await validateOAuthEndpointUrl(endpoint);
} catch (e) {
if (e instanceof OAuthSecurityError && e.message.includes('resolves to private network address')) {
// check dig/nslookup from this host; remove hosts-file or split-horizon overrides mapping the name to private IPs
}
throw e;
} Prevention
- Never map public-looking hostnames to private IPs in /etc/hosts for testing — use loopback URLs with allowLoopback
- Be aware of split-horizon DNS in corporate networks; run clients where the name resolves publicly
- If a mismatch appears suddenly, verify DNS integrity (possible rebinding attack) before bypassing
When it happens
Trigger: Passing a public hostname whose DNS points at a private IP — e.g. a hosts-file entry mapping auth.example.com to 127.0.0.1 or 192.168.x.x, split-horizon DNS returning internal addresses, or a load-balancer record resolving to an internal range (the lookup uses all: true, so one bad record among many triggers the block).
Common situations: Corporate split-horizon DNS where the same name resolves internally to a 10.x/192.168.x address; /etc/hosts overrides left over from local testing; DNS rebinding attack attempts; testing with public-looking hostnames that map to local/docker IPs.
Related errors
- OAuth endpoint "${resolvedUrl}" points to private or reserve
- Failed to resolve hostname "${hostname}" for OAuth endpoint
- DNS lookup failed for OAuth endpoint host "${hostname}": ${g
- Failed to resolve relative OAuth URL "${urlStr}" against bas
- Invalid OAuth endpoint protocol "${parsed.protocol}". Only H
AI-assisted analysis of google-gemini/gemini-cli@3c311beac2 (2026-08-27).
Data as JSON: /api/errors/e7133ee03a13438d.
Report an issue: GitHub.