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+
114import json
215import logging
316
1629 AgentCard ,
1730)
1831from 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
2137logger = 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