feat(a2a): add deep SSRF validation for all transport URLs in agent card

This commit is contained in:
Alisa Novikova
2026-03-05 17:00:25 -08:00
parent a42fdee8a8
commit fb60eecba0
2 changed files with 56 additions and 1 deletions

View File

@@ -407,6 +407,25 @@ describe('A2AClientManager', () => {
);
});
it('should throw if a public agent card contains a private transport URL (Deep SSRF protection)', async () => {
const publicUrl = 'https://public.agent.com/card.json';
const resolverInstance = {
resolve: vi.fn().mockResolvedValue({
...mockAgentCard,
url: 'http://192.168.1.1/api', // Malicious private transport in public card
} as AgentCard),
};
vi.mocked(sdkClient.DefaultAgentCardResolver).mockReturnValue(
resolverInstance as unknown as sdkClient.DefaultAgentCardResolver,
);
await expect(
manager.loadAgent('malicious-agent', publicUrl),
).rejects.toThrow(
/contains transport URL pointing to private IP range: http:\/\/192.168.1.1\/api/,
);
});
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';

View File

@@ -282,6 +282,42 @@ export class A2AClientManager {
}
const rawCard = await resolver.resolve(baseUrl, path);
return normalizeAgentCard(rawCard);
const agentCard = normalizeAgentCard(rawCard);
// Deep validation of all transport URLs within the card to prevent SSRF
this.validateAgentCardUrls(agentName, agentCard);
return agentCard;
}
/**
* Validates all URLs (top-level and interfaces) within an AgentCard for SSRF.
*/
private validateAgentCardUrls(agentName: string, card: AgentCard): void {
const urlsToValidate = [card.url];
if (card.additionalInterfaces) {
for (const intf of card.additionalInterfaces) {
if (intf.url) urlsToValidate.push(intf.url);
}
}
for (const url of urlsToValidate) {
if (!url) continue;
// Ensure URL has a scheme for the parser (gRPC often provides raw IP:port)
const validationUrl = url.includes('://') ? url : `http://${url}`;
if (isPrivateIp(validationUrl)) {
const parsed = new URL(validationUrl);
if (
parsed.hostname !== 'localhost' &&
parsed.hostname !== '127.0.0.1'
) {
throw new Error(
`Refusing to load agent '${agentName}': contains transport URL pointing to private IP range: ${url}.`,
);
}
}
}
}
}