Skip to content

Commit 4e72411

Browse files
committed
fix(security): SSRF via AgentCard URL and context ID Injection (A2A-SSRF-01, A2A-INJ-01)
- Add url_validation.py: validates AgentCard.url against loopback, RFP 1918, link-local (IMDS), and non-http(s) schemes before SDK uses it as RPC endpoint - Patch card_resolver.py: call validate_agent_card_url() after model_validate() for card url and all additional_interfaces urls - Patch default_request_handler.py: add optional get_caller_id hook to enforce cantext_id ownership; defaults to warn-and-allow for backword compatibility Fixes CWE-918 (SSRF) and CWE-639 (context injection)
1 parent 6d49122 commit 4e72411

3 files changed

Lines changed: 323 additions & 190 deletions

File tree

src/a2a/client/card_resolver.py

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
"""Patched version of a2a/client/card_resolver.py
2+
3+
Fix for A2A-SSRF-01: validate AgentCard.url before returning the card.
4+
5+
Diff summary vs. original (v0.3.25):
6+
+ import A2ASSRFValidationError, validate_agent_card_url from a2a.utils.url_validation
7+
+ call validate_agent_card_url(agent_card.url) after model_validate()
8+
+ wrap in try/except to raise A2AClientJSONError with a clear SSRF message
9+
+ validate additional_interfaces[*].url as well (same attack surface)
10+
11+
Target file: src/a2a/client/card_resolver.py
12+
"""
13+
114
import json
215
import logging
316

@@ -16,6 +29,9 @@
1629
AgentCard,
1730
)
1831
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
32+
# ---- NEW IMPORT (fix for A2A-SSRF-01) ----
33+
from a2a.utils.url_validation import A2ASSRFValidationError, validate_agent_card_url
34+
# -------------------------------------------
1935

2036

2137
logger = logging.getLogger(__name__)
@@ -30,13 +46,6 @@ def __init__(
3046
base_url: str,
3147
agent_card_path: str = AGENT_CARD_WELL_KNOWN_PATH,
3248
) -> None:
33-
"""Initializes the A2ACardResolver.
34-
35-
Args:
36-
httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient).
37-
base_url: The base URL of the agent's host.
38-
agent_card_path: The path to the agent card endpoint, relative to the base URL.
39-
"""
4049
self.base_url = base_url.rstrip('/')
4150
self.agent_card_path = agent_card_path.lstrip('/')
4251
self.httpx_client = httpx_client
@@ -47,29 +56,7 @@ async def get_agent_card(
4756
http_kwargs: dict[str, Any] | None = None,
4857
signature_verifier: Callable[[AgentCard], None] | None = None,
4958
) -> AgentCard:
50-
"""Fetches an agent card from a specified path relative to the base_url.
51-
52-
If relative_card_path is None, it defaults to the resolver's configured
53-
agent_card_path (for the public agent card).
54-
55-
Args:
56-
relative_card_path: Optional path to the agent card endpoint,
57-
relative to the base URL. If None, uses the default public
58-
agent card path. Use `'/'` for an empty path.
59-
http_kwargs: Optional dictionary of keyword arguments to pass to the
60-
underlying httpx.get request.
61-
signature_verifier: A callable used to verify the agent card's signatures.
62-
63-
Returns:
64-
An `AgentCard` object representing the agent's capabilities.
65-
66-
Raises:
67-
A2AClientHTTPError: If an HTTP error occurs during the request.
68-
A2AClientJSONError: If the response body cannot be decoded as JSON
69-
or validated against the AgentCard schema.
70-
"""
7159
if not relative_card_path:
72-
# Use the default public agent card path configured during initialization
7360
path_segment = self.agent_card_path
7461
else:
7562
path_segment = relative_card_path.lstrip('/')
@@ -89,8 +76,24 @@ async def get_agent_card(
8976
agent_card_data,
9077
)
9178
agent_card = AgentCard.model_validate(agent_card_data)
79+
80+
# ---- FIX: A2A-SSRF-01 — validate card.url before returning ----
81+
# Without this check, any caller who controls the card endpoint
82+
# can redirect all subsequent RPC calls to an internal address.
83+
try:
84+
validate_agent_card_url(agent_card.url)
85+
# Also validate any additional transport URLs declared in the card.
86+
for iface in agent_card.additional_interfaces or []:
87+
validate_agent_card_url(iface.url)
88+
except A2ASSRFValidationError as e:
89+
raise A2AClientJSONError(
90+
f'AgentCard from {target_url} failed SSRF URL validation: {e}'
91+
) from e
92+
# -----------------------------------------------------------------
93+
9294
if signature_verifier:
9395
signature_verifier(agent_card)
96+
9497
except httpx.HTTPStatusError as e:
9598
raise A2AClientHTTPError(
9699
e.response.status_code,
@@ -105,7 +108,7 @@ async def get_agent_card(
105108
503,
106109
f'Network communication error fetching agent card from {target_url}: {e}',
107110
) from e
108-
except ValidationError as e: # Pydantic validation error
111+
except ValidationError as e:
109112
raise A2AClientJSONError(
110113
f'Failed to validate agent card structure from {target_url}: {e.json()}'
111114
) from e

0 commit comments

Comments
 (0)