Skip to content

fix(security): SSRF via AgentCard URL and context ID Injection (A2A-SSRF-01, A2A-INJ-01)#895

Open
amit-raut wants to merge 8 commits intoa2aproject:mainfrom
amit-raut:fix/ssrf-agentcard-url-context-injection
Open

fix(security): SSRF via AgentCard URL and context ID Injection (A2A-SSRF-01, A2A-INJ-01)#895
amit-raut wants to merge 8 commits intoa2aproject:mainfrom
amit-raut:fix/ssrf-agentcard-url-context-injection

Conversation

@amit-raut
Copy link
Copy Markdown

Security Fix: SSRF via AgentCard URL & Context ID Injection (A2A-SSRF-01, A2A-INJ-01)

Summary

This PR addresses two security vulnerabilities confirmed against a2a-sdk v0.3.25:

ID CWE CVSS v3.1 Short description
A2A-SSRF-01 CWE-918 9.1 Critical AgentCard.url used as RPC endpoint with no validation — SSRF to internal networks
A2A-INJ-01 CWE-639 8.6 High message.contextId accepted without ownership check — cross-user context injection

Changes

Fix 1 — A2A-SSRF-01: URL validation for AgentCard (new file + patch)

New file: src/a2a/utils/url_validation.py

Adds validate_agent_card_url(url: str) -> None which:

  • Requires http or https scheme (blocks file://, gopher://, etc.)
  • Resolves the hostname via socket.getaddrinfo and rejects addresses within:
    • Loopback (127.0.0.0/8, ::1)
    • RFC 1918 private ranges (10/8, 172.16/12, 192.168/16)
    • Link-local / IMDS (169.254.0.0/16, fe80::/10) — covers AWS/GCP/Azure metadata service
    • IPv6 ULA (fc00::/7), shared address space (100.64.0.0/10), and other IANA reserved blocks
  • Raises A2ASSRFValidationError (subclass of ValueError) with a descriptive message

Patched: src/a2a/client/card_resolver.py

Calls validate_agent_card_url() after AgentCard.model_validate() in
get_agent_card(), before returning the card to the caller. Also validates
all additional_interfaces[*].url values (same attack surface).

# After: agent_card = AgentCard.model_validate(agent_card_data)
try:
    validate_agent_card_url(agent_card.url)
    for iface in agent_card.additional_interfaces or []:
        validate_agent_card_url(iface.url)
except A2ASSRFValidationError as e:
    raise A2AClientJSONError(
        f'AgentCard from {target_url} failed SSRF URL validation: {e}'
    ) from e

Why here: The card resolver is the single choke point where all remote
cards enter the SDK. Validating here covers ClientFactory.connect(),
direct A2ACardResolver usage, and any future client code that calls
get_agent_card().


Fix 2 — A2A-INJ-01: Context ownership policy hook

Patched: src/a2a/server/request_handlers/default_request_handler.py

Adds an optional get_caller_id parameter to DefaultRequestHandler.__init__:

def __init__(
    self,
    agent_executor: AgentExecutor,
    task_store: TaskStore,
    ...
    get_caller_id: Callable[[ServerCallContext | None], str | None] | None = None,  # NEW
) -> None:

In _setup_message_execution, the first caller for a given context_id is
recorded in _context_owners. Subsequent requests for the same context from a
different identity are rejected with ServerError before any task lookup:

# Before task_manager.get_task() — prevents bypass via new task_id
if params.message.context_id:
    self._check_context_ownership(params.message.context_id, context)

Default behaviour (backward compatible): When get_caller_id is not
provided, the handler logs a WARNING and allows the request through.
Existing deployments are unaffected.

Production use: Supply any callable that extracts a stable identity string
from ServerCallContext (JWT sub, mTLS cert fingerprint, API key header, etc.).

Why a hook rather than hard enforcement: The A2A spec does not yet define
a normative identity model. A hook lets each deployment enforce ownership
using whatever identity mechanism they have, without the SDK imposing a
specific auth scheme. The warning log ensures unenforced deployments are
visibly alerted.


Testing

SSRF validation (tests/utils/test_url_validation.py)

Parametrize validate_agent_card_url over blocked inputs (loopback 127.0.0.1,
localhost, RFC 1918 ranges, AWS IMDS 169.254.169.254, IPv6 ::1,
file://, gopher://) and assert A2ASSRFValidationError is raised.
Assert no exception for https://agent.example.com/rpc.

Context ownership integration test

Construct a DefaultRequestHandler with a get_caller_id extractor.
Send a first message as "alice" to establish context ownership.
Send a second message referencing the same context_id with caller "attacker" and
assert a ServerError is raised. Verify the policy was invoked exactly once.


Backward Compatibility

Change Backward compatible?
validate_agent_card_url (new module) Yes — new file, no existing code changed
get_agent_card raises A2AClientJSONError for blocked URLs Breaking for callers that rely on fetching cards from private IPs (should not be a legitimate use case)
get_caller_id parameter Yes — optional, defaults to warn-and-allow

References

Checklist

  • Follow the CONTRIBUTING Guide.
  • Make your Pull Request title in the https://www.conventionalcommits.org/ specification.
    • Important Prefixes for release-please:
      • fix: which represents bug fixes, and correlates to a SemVer patch.
      • feat: represents a new feature, and correlates to a SemVer minor.
      • feat!:, or fix!:, refactor!:, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.
  • Ensure the tests and linter pass (Run bash scripts/format.sh from the repository root to format)
  • Appropriate docs were updated (if necessary)

Fixes None 🦕

…SRF-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)
@amit-raut amit-raut requested a review from a team as a code owner March 25, 2026 03:56
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security posture of the a2a-sdk by addressing two critical vulnerabilities. It introduces comprehensive validation for agent card URLs to mitigate Server-Side Request Forgery risks and provides a flexible mechanism for enforcing context ownership, safeguarding against unauthorized manipulation of user contexts. These changes ensure that the SDK operates with greater integrity and resilience against common attack vectors.

Highlights

  • SSRF Protection for AgentCard URLs (A2A-SSRF-01): Implemented robust URL validation for AgentCard.url and additional_interfaces[*].url to prevent Server-Side Request Forgery (SSRF) attacks. This validation, performed in get_agent_card(), ensures that agent card URLs do not resolve to internal, loopback, or other reserved IP addresses, blocking attempts to access sensitive internal network resources.
  • Context Ownership Enforcement (A2A-INJ-01): Introduced an optional get_caller_id parameter to DefaultRequestHandler to enable context-level ownership tracking. When configured, the handler records the identity of the caller who initiated a context_id and rejects subsequent messages to that context_id from different, unauthorized callers, thereby preventing cross-user context injection.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant security enhancements by addressing two vulnerabilities: A2A-SSRF-01 and A2A-INJ-01. For A2A-SSRF-01, a new url_validation.py module is added to validate agent card URLs against private or reserved IP ranges, and this validation is integrated into card_resolver.py. For A2A-INJ-01, context-level ownership tracking is implemented in default_request_handler.py to prevent unauthorized message injection into existing contexts. Feedback includes a high-severity issue where the SSRF warning is not logged if get_caller_id is not configured, suggesting it be moved to __init__. There's also a medium-severity concern about the in-memory nature of _context_owners, which could lead to context hijacking after server restarts, suggesting persistence. Additionally, several low-severity comments recommend restoring removed docstrings for maintainability, addressing unreachable code in _check_context_ownership, and changing _BLOCKED_NETWORKS to a tuple for immutability.

- Move context ownership warning to __init__ (was unreachable)
- Remove unreachable owner-is-None guard in _check_context_ownership
- Change _BLOCKED_NETWORKS to tuple for immutability
- Restore dropped docstrings in card_resolver and
default_request_handler
- Fix non-ASCII chars in comments (use -> and --)
- Add tests/utils/test_url_validation.py with 26 SSRF validation tests
Mock(spec=AgentCard) with Pydantic v2 does not expose field attributes.
Our SSRF patch accesses agent_card.url after model_validate(), add url
and additional_interfaces to all Mock(spec=AgentCard) instances
so the attribute access succeeds.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 27, 2026

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/client/card_resolver.py 100.00% 95.00% 🔴 -5.00%
src/a2a/server/request_handlers/default_request_handler.py 97.34% 90.70% 🔴 -6.64%
src/a2a/utils/url_validation.py (new) 89.19%
Total 91.73% 91.32% 🔴 -0.41%

Generated by coverage-comment.yml

- Consolidate two near-identical conftest files into tests/conftest.py
  to fix JSCPD copy-paste detection failure
- Add ASSRF, canonname, gaierror, IMDS, INJ, sockaddr to spell-check
  allow list (all come from our url_validation.py patch)
…patch

- Remove module-level research docstrings from card_resolver.py and
    default_request_handler.py (originals have none; D415 lint failure)
- Add missing docstrings to five push-notification handler methods
    that existed in the original SDK (D102 lint failure)
- Fix import ordering in card_resolver.py and url_validation.py (I001)
- Convert double-quoted string literals to single quotes throughout
    both patched source files (Q000; project uses quote-style = single)
- Restore parentheses in except clause:
    except (asyncio.CancelledError, GeneratorExit):
    Bare tuple syntax was added in Python 3.14; CI targets Python 3.10
    where it raises SyntaxError (ruff had silently removed the parens)
Move pydantic import to correct position per project isort config
Fix import ordring and covert double quoted to single quotes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant