Skip to content

Commit 6b2f24c

Browse files
committed
fix(mcp): add SSRF protection to OAuth metadata discovery
The OAuth discovery flow in oauth-utils.ts and oauth-provider.ts fetches URLs received directly from external MCP server responses without any SSRF validation, unlike web-fetch.ts which already uses isLoopbackHost() and resolveAndValidateDns() from utils/fetch.ts. A malicious MCP server can return private/internal IPs in: - WWW-Authenticate: Bearer resource_metadata="http://169.254.169.254/..." - /.well-known/oauth-protected-resource: {authorization_servers: ["http://169.254.169.254"]} - /.well-known/oauth-authorization-server: {registration_endpoint: "http://10.x.x.x/admin"} This causes gemini-cli to make GET and POST requests to cloud IMDS endpoints (AWS 169.254.169.254, GCP metadata.google.internal, Azure 169.254.169.254) or internal services on the developer's network. Fix: apply the same isLoopbackHost() + resolveAndValidateDns() guards already used in web-fetch.ts to: - OAuthUtils.fetchProtectedResourceMetadata() - OAuthUtils.fetchAuthorizationServerMetadata() - MCPOAuthProvider.registerClient()
1 parent 17a70c7 commit 6b2f24c

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

packages/core/src/mcp/oauth-provider.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { OAuthUtils, ResourceMismatchError } from './oauth-utils.js';
1414
import { coreEvents } from '../utils/events.js';
1515
import { debugLogger } from '../utils/debugLogger.js';
1616
import { getConsentForOauth } from '../utils/authConsent.js';
17+
import { isLoopbackHost, resolveAndValidateDns } from '../utils/fetch.js';
1718
import {
1819
generatePKCEParams,
1920
startCallbackServer,
@@ -111,6 +112,18 @@ export class MCPOAuthProvider {
111112
scope: config.scopes?.join(' ') || '',
112113
};
113114

115+
const parsedRegUrl = new URL(registrationUrl);
116+
if (isLoopbackHost(parsedRegUrl.hostname)) {
117+
throw new Error(
118+
`Client registration blocked: loopback address not allowed: ${registrationUrl}`,
119+
);
120+
}
121+
const resolvedAddrs = await resolveAndValidateDns(registrationUrl);
122+
if (resolvedAddrs.length === 0) {
123+
throw new Error(
124+
`Client registration blocked: private/reserved IP not allowed: ${registrationUrl}`,
125+
);
126+
}
114127
const response = await fetch(registrationUrl, {
115128
method: 'POST',
116129
headers: {

packages/core/src/mcp/oauth-utils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
import type { MCPOAuthConfig } from './oauth-provider.js';
88
import { getErrorMessage } from '../utils/errors.js';
99
import { debugLogger } from '../utils/debugLogger.js';
10+
import {
11+
isLoopbackHost,
12+
resolveAndValidateDns,
13+
} from '../utils/fetch.js';
1014

1115
/**
1216
* Error thrown when the discovered resource metadata does not match the expected resource.
@@ -97,6 +101,20 @@ export class OAuthUtils {
97101
resourceMetadataUrl: string,
98102
): Promise<OAuthProtectedResourceMetadata | null> {
99103
try {
104+
const parsedUrl = new URL(resourceMetadataUrl);
105+
if (isLoopbackHost(parsedUrl.hostname)) {
106+
debugLogger.debug(
107+
`Blocked OAuth metadata fetch to loopback address: ${resourceMetadataUrl}`,
108+
);
109+
return null;
110+
}
111+
const resolvedAddrs = await resolveAndValidateDns(resourceMetadataUrl);
112+
if (resolvedAddrs.length === 0) {
113+
debugLogger.debug(
114+
`Blocked OAuth metadata fetch to private/reserved IP: ${resourceMetadataUrl}`,
115+
);
116+
return null;
117+
}
100118
const response = await fetch(resourceMetadataUrl);
101119
if (!response.ok) {
102120
return null;
@@ -121,6 +139,20 @@ export class OAuthUtils {
121139
authServerMetadataUrl: string,
122140
): Promise<OAuthAuthorizationServerMetadata | null> {
123141
try {
142+
const parsedUrl = new URL(authServerMetadataUrl);
143+
if (isLoopbackHost(parsedUrl.hostname)) {
144+
debugLogger.debug(
145+
`Blocked OAuth metadata fetch to loopback address: ${authServerMetadataUrl}`,
146+
);
147+
return null;
148+
}
149+
const resolvedAddrs = await resolveAndValidateDns(authServerMetadataUrl);
150+
if (resolvedAddrs.length === 0) {
151+
debugLogger.debug(
152+
`Blocked OAuth metadata fetch to private/reserved IP: ${authServerMetadataUrl}`,
153+
);
154+
return null;
155+
}
124156
const response = await fetch(authServerMetadataUrl);
125157
if (!response.ok) {
126158
return null;

0 commit comments

Comments
 (0)