Skip to content
63 changes: 33 additions & 30 deletions src/a2a/client/card_resolver.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
"""Patched version of a2a/client/card_resolver.py

Fix for A2A-SSRF-01: validate AgentCard.url before returning the card.

Diff summary vs. original (v0.3.25):
+ import A2ASSRFValidationError, validate_agent_card_url from a2a.utils.url_validation

Check failure on line 6 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Check Spelling

`ASSRF` is not a recognized word. (unrecognized-spelling)
+ call validate_agent_card_url(agent_card.url) after model_validate()
+ wrap in try/except to raise A2AClientJSONError with a clear SSRF message
+ validate additional_interfaces[*].url as well (same attack surface)

Target file: src/a2a/client/card_resolver.py
"""

Check failure on line 12 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

ruff (D415)

src/a2a/client/card_resolver.py:1:1: D415 First line should end with a period, question mark, or exclamation point help: Add closing punctuation

import json
import logging

from collections.abc import Callable
from typing import Any

import httpx

from pydantic import ValidationError

from a2a.client.errors import (
A2AClientHTTPError,
A2AClientJSONError,
)
from a2a.types import (
AgentCard,
)
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
# ---- NEW IMPORT (fix for A2A-SSRF-01) ----
from a2a.utils.url_validation import A2ASSRFValidationError, validate_agent_card_url

Check failure on line 33 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Check Spelling

`ASSRF` is not a recognized word. (unrecognized-spelling)

Check warning on line 33 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Check Spelling

`ASSRF` is not a recognized word. (unrecognized-spelling)

Check failure on line 33 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

ruff (I001)

src/a2a/client/card_resolver.py:14:1: I001 Import block is un-sorted or un-formatted help: Organize imports
# -------------------------------------------


logger = logging.getLogger(__name__)
Expand All @@ -30,46 +46,17 @@
base_url: str,
agent_card_path: str = AGENT_CARD_WELL_KNOWN_PATH,
) -> None:
"""Initializes the A2ACardResolver.

Args:
httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient).
base_url: The base URL of the agent's host.
agent_card_path: The path to the agent card endpoint, relative to the base URL.
"""
self.base_url = base_url.rstrip('/')
self.agent_card_path = agent_card_path.lstrip('/')
self.httpx_client = httpx_client

async def get_agent_card(

Check failure on line 53 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

ruff (D102)

src/a2a/client/card_resolver.py:53:15: D102 Missing docstring in public method
self,
relative_card_path: str | None = None,
http_kwargs: dict[str, Any] | None = None,
signature_verifier: Callable[[AgentCard], None] | None = None,
) -> AgentCard:
"""Fetches an agent card from a specified path relative to the base_url.

If relative_card_path is None, it defaults to the resolver's configured
agent_card_path (for the public agent card).

Args:
relative_card_path: Optional path to the agent card endpoint,
relative to the base URL. If None, uses the default public
agent card path. Use `'/'` for an empty path.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.get request.
signature_verifier: A callable used to verify the agent card's signatures.

Returns:
An `AgentCard` object representing the agent's capabilities.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON
or validated against the AgentCard schema.
"""
Comment thread
amit-raut marked this conversation as resolved.
if not relative_card_path:
# Use the default public agent card path configured during initialization
path_segment = self.agent_card_path
else:
path_segment = relative_card_path.lstrip('/')
Expand All @@ -89,8 +76,24 @@
agent_card_data,
)
agent_card = AgentCard.model_validate(agent_card_data)

# ---- FIX: A2A-SSRF-01 — validate card.url before returning ----
# Without this check, any caller who controls the card endpoint
# can redirect all subsequent RPC calls to an internal address.
try:
validate_agent_card_url(agent_card.url)
# Also validate any additional transport URLs declared in the card.
for iface in agent_card.additional_interfaces or []:
validate_agent_card_url(iface.url)
except A2ASSRFValidationError as e:

Check failure on line 88 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Check Spelling

`ASSRF` is not a recognized word. (unrecognized-spelling)

Check warning on line 88 in src/a2a/client/card_resolver.py

View workflow job for this annotation

GitHub Actions / Check Spelling

`ASSRF` is not a recognized word. (unrecognized-spelling)
raise A2AClientJSONError(
f'AgentCard from {target_url} failed SSRF URL validation: {e}'
) from e
# -----------------------------------------------------------------

if signature_verifier:
signature_verifier(agent_card)

except httpx.HTTPStatusError as e:
raise A2AClientHTTPError(
e.response.status_code,
Expand All @@ -105,7 +108,7 @@
503,
f'Network communication error fetching agent card from {target_url}: {e}',
) from e
except ValidationError as e: # Pydantic validation error
except ValidationError as e:
raise A2AClientJSONError(
f'Failed to validate agent card structure from {target_url}: {e.json()}'
) from e
Expand Down
Loading
Loading