mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 06:31:01 -07:00
feat(a2a): implement robust URL parsing and SSRF protection for agent cards
This commit is contained in:
@@ -399,5 +399,31 @@ describe('A2AClientManager', () => {
|
||||
'.well-known/agent-card.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if a remote agent uses a private IP (SSRF protection)', async () => {
|
||||
const privateUrl = 'http://169.254.169.254/.well-known/agent-card.json';
|
||||
await expect(manager.loadAgent('ssrf-agent', privateUrl)).rejects.toThrow(
|
||||
/Refusing to load agent 'ssrf-agent' from private IP range/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle URLs where .well-known appears in the domain/subdomain', async () => {
|
||||
const trickyUrl =
|
||||
'http://.well-known/agent-card.json.attacker.com/my-agent';
|
||||
const resolverInstance = {
|
||||
resolve: vi.fn().mockResolvedValue({ name: 'test' } as AgentCard),
|
||||
};
|
||||
vi.mocked(sdkClient.DefaultAgentCardResolver).mockReturnValue(
|
||||
resolverInstance as unknown as sdkClient.DefaultAgentCardResolver,
|
||||
);
|
||||
|
||||
await manager.loadAgent('tricky-agent', trickyUrl);
|
||||
|
||||
// Should treat the whole thing as baseUrl since it doesn't end with standardPath
|
||||
expect(resolverInstance.resolve).toHaveBeenCalledWith(
|
||||
trickyUrl,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GrpcTransportFactory } from '@a2a-js/sdk/client/grpc';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Agent as UndiciAgent } from 'undici';
|
||||
import { getGrpcCredentials, normalizeAgentCard } from './a2aUtils.js';
|
||||
import { isPrivateIp } from '../utils/fetch.js';
|
||||
import { debugLogger } from '../utils/debugLogger.js';
|
||||
|
||||
// Remote agents can take 10+ minutes (e.g. Deep Research).
|
||||
@@ -97,7 +98,7 @@ export class A2AClientManager {
|
||||
|
||||
const fetchImpl = this.getFetchImpl(authHandler);
|
||||
const resolver = new DefaultAgentCardResolver({ fetchImpl });
|
||||
const agentCard = await this.resolveAgentCard(agentCardUrl, resolver);
|
||||
const agentCard = await this.resolveAgentCard(name, agentCardUrl, resolver);
|
||||
|
||||
// Configure standard SDK client for tool registration and discovery
|
||||
const clientOptions = ClientFactoryOptions.createFrom(
|
||||
@@ -247,8 +248,10 @@ export class A2AClientManager {
|
||||
/**
|
||||
* Resolves and normalizes an agent card from a given URL.
|
||||
* Handles splitting the URL if it already contains the standard .well-known path.
|
||||
* Also performs basic SSRF validation to prevent internal IP access.
|
||||
*/
|
||||
private async resolveAgentCard(
|
||||
agentName: string,
|
||||
url: string,
|
||||
resolver: DefaultAgentCardResolver,
|
||||
): Promise<AgentCard> {
|
||||
@@ -256,10 +259,26 @@ export class A2AClientManager {
|
||||
let baseUrl = url;
|
||||
let path: string | undefined;
|
||||
|
||||
if (baseUrl.includes(standardPath)) {
|
||||
const parts = baseUrl.split(standardPath);
|
||||
baseUrl = parts[0] || '';
|
||||
path = standardPath;
|
||||
// Validate URL to prevent SSRF
|
||||
if (isPrivateIp(url)) {
|
||||
// Local/private IPs are allowed ONLY for localhost for testing.
|
||||
const parsed = new URL(url);
|
||||
if (parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
|
||||
throw new Error(
|
||||
`Refusing to load agent '${agentName}' from private IP range: ${url}. Remote agents must use public URLs.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (parsedUrl.pathname.endsWith(standardPath)) {
|
||||
// Correctly split the URL into baseUrl and standard path
|
||||
path = standardPath;
|
||||
baseUrl = url.substring(0, url.lastIndexOf(standardPath));
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid agent card URL: ${url}`, { cause: e });
|
||||
}
|
||||
|
||||
const rawCard = await resolver.resolve(baseUrl, path);
|
||||
|
||||
@@ -22,6 +22,7 @@ setGlobalDispatcher(
|
||||
const PRIVATE_IP_RANGES = [
|
||||
/^10\./,
|
||||
/^127\./,
|
||||
/^169\.254\./,
|
||||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
||||
/^192\.168\./,
|
||||
/^::1$/,
|
||||
|
||||
Reference in New Issue
Block a user