Repository navigation
[Task]: Prepare URL validation infrastructure for agent card and webhook SSRF protection #1023
Copy link
Copy link
Open
Open
Copy link
Labels
component: coreIssues related to base data models, auth, gRPC interfaces, observability, and fundamental utilities.Issues related to base data models, auth, gRPC interfaces, observability, and fundamental utilities.
Description
Context
We have two places which can benefit from SSRF protection:
- Client: fetching agent cards and using URLs to make A2A calls (brought in fix(security): SSRF via AgentCard URL and context ID Injection (A2A-SSRF-01, A2A-INJ-01) #895, filed a separate issue [Feat]: Agent card validation hook #975).
- Push notification webhooks URLs (Security Improvement: Add SSRF protection for Push Notification webhooks and authorization checks for Task operations #786): there is a spec statement about SSRF protection there.
Description
Both cases above can share domain agnostic machinery for URL validation.
The following requirements apply:
- Composable: local addresses are perfectly fine for some deployments.
- Validation should happen before the actual invocation and invocation should use "pinned" IP to protect from DNS rebinding.
- Built-in rules:
RequireScheme(to restrict HTTP(S)-only or HTTPS-only),BlockPrivateNetworks(with allowlist for deployments where using private networks is a legitimate use-case).
This issue scopes to UrlValidator only and should be a foundation for #975 and #786 where appropriate domain validators are going to be implemented.
flowchart TB
subgraph Wrappers["Domain wrappers (own which fields to validate + domain error type)"]
direction LR
PN["PushNotificationUrlValidator"]
AC["AgentCardUrlValidator"]
end
V["UrlValidator<br/>parse → resolve → run rules in order<br/>(rule raises = reject, returns = continue)"]
subgraph Rules["Composable rules"]
direction LR
RS["RequireScheme"]
BPN["BlockPrivateNetworks<br/>(allow_hosts, allow_cidrs)"]
CR["…custom…"]
end
PN --> V
AC --> V
V --> Rules
class PN,AC wrap
class V core
class RS,BPN,HD,CR rule
Sketch:
class InvalidUrlError(ValueError):
"""Raised by UrlValidator.validate when a URL is rejected."""
class UrlValidationRule(ABC):
@abstractmethod
async def check(self, url: ResolvedUrl) -> None:
"""Raise InvalidUrlError to reject url."""
class ResolvedUrl:
raw: str
parsed: SplitResult
addresses: tuple[IpAddress, ...]
class UrlValidator:
def __init__(
self,
rules: Sequence[UrlValidationRule] = (),
resolve: bool = True,
) -> None:
. . .
async def validate(self, url: str) -> ResolvedUrl:
# Conditionally resolves IPs via getaddrinfo
resolved = await self._build(url)
# Just runs all rules one by one, they throw to stop
for rule in self._rules:
await rule.check(resolved)
# Returns URL so that pinned address can be used
return resolvedReactions are currently unavailable
Metadata
Metadata
Assignees
Labels
component: coreIssues related to base data models, auth, gRPC interfaces, observability, and fundamental utilities.Issues related to base data models, auth, gRPC interfaces, observability, and fundamental utilities.
Activity
Ryujiyasu commentedon Apr 28, 2026
Thanks for spinning this out, @ishymko — happy to see the shared infrastructure land before the per-domain wrappers. A few thoughts from the threat-modeling side of #786:
1. How does
ResolvedUrl.addressesactually get used by the caller?The sketch returns the resolved addresses but doesn't pin httpx to them. Without an explicit pinning mechanism (e.g., a custom
httpx.BaseTransportthat rewritesrequest.url.hostto the validated IP while preserving theHostheader and TLS SNI — see Drawbridge for prior art), there's still a TOCTOU window betweengetaddrinfoand the actualsocket.connect. Worth making this part of the contract, not an exercise for downstream implementers.2. Redirect handling.
The current design is silent on redirects. Established practice (GitLab, Drawbridge, Advocate) is to re-validate every hop, not disable redirects. pyload-ng's CVE-2026-35459 is a recent reminder of what happens when this is skipped. Suggest the validator either owns the redirect loop or documents that callers MUST run each
Locationback throughvalidate().3.
BlockPrivateNetworksis not enough on its own.ipaddress.IPv6Address.is_privatehas known gaps that an SSRF-focused blocklist needs to handle explicitly:64:ff9b::/96(64:ff9b::a9fe:a9fereaches AWS IMDS butis_private=False)2002::/16(embedded IPv4 not extracted by.ipv4_mapped)100.64.0.0/10(covers Alibaba metadata100.100.100.200)And cloud metadata endpoints worth treating as first-class blocks even though some are technically link-local:
169.254.170.2192.0.0.192(RFC 7600 — not link-local!)100.100.100.200(CGNAT — not RFC 1918)A
BlockCloudMetadatarule alongsideBlockPrivateNetworksmight be worth surfacing given how often this is the actual exploit target.4.
getaddrinforesolution policy.Suggest
getaddrinfo(host, None, AF_UNSPEC)and rejecting if any returned address fails — Craft CMS GHSA-v2gc-rm6g-wrw9 is the canonical example of what goes wrong when only the v4 record is checked.Happy to help draft the test matrix for the Push Notification wrapper once the core validator lands. Also happy to take a stab at a PR for either side if that's useful — let me know what would fit your workflow.
matanweks commentedon May 10, 2026
Happy to take this. Quick alignment before I push code:
a2a.security.url_validatorsor keepa2a.utils.url_validation(where PR fix(security): SSRF via AgentCard URL and context ID Injection (A2A-SSRF-01, A2A-INJ-01) #895 currently puts it)?UrlValidatorown the loop (re-validate eachLocation) or document that callers must run each hop throughvalidate()? Leaning toward the former per Ryujiyasu's note about CVE-2026-35459.BlockCloudMetadataas a separate built-in rule (covers169.254.170.2,192.0.0.192,100.100.100.200), or fold intoBlockPrivateNetworks?On overlap with #895: I'll structure around your composable-rules sketch; happy to update if #895 lands first or gets superseded.
feat: add composable URL validation utilities
feat: add URL validation infrastructure for SSRF protection (a2aproje…
feat: add URL validation infrastructure for SSRF protection (a2aproje…
feat: add URL validation infrastructure for SSRF protection (a2aproje…
kokotatan commentedon Oct 4, 2026
@ishymko A scope question before anyone picks this up again. Since this was filed, push-notification URL checks landed as
a2a.utils.push_url_validator(#1164, #1173), and #975 is nowmaintainers-only. The last update here was in May.Is this issue still open to external contributors? If so, would you want it as a refactor that extracts the generic part of
push_url_validatorinto the composableUrlValidator/ rules from your sketch (with the push path switched over and its behavior unchanged), leaving the agent-card wrapper for #975 to you? Or would you rather keep it with the maintainers too?Linux2010 commentedon Oct 10, 2026
@ishymko — adding my voice to @kokotatan's scope question above, plus a concrete starting point: I've opened a draft PR implementing your sketch as-is (#1333), so the discussion can happen against working code.
What the draft contains:
UrlValidatorcore (parse → optional resolve → rules in order → returnsResolvedUrlwith the resolved addresses, per your sketch)RequireScheme+BlockPrivateNetworks(allow_hosts/allow_cidrs) as the first built-in rulesPushNotificationUrlValidator:validate_push_notification_urlnow delegates to the composable stack — boolean, fail-closed contract unchanged; the existing SSRF tests pass unmodifiedAgentCardUrlValidator: agent card URLs screened (http/https, public by default, private networks opt-in) — the remaining [Task]: Prepare URL validation infrastructure for agent card and webhook SSRF protection #1023 scope beyond pushQuestions before I mark it ready:
a2a/utils/url_validation.py(next topush_url_validator) vs a dedicated namespace — any preference?ResolvedUrlalready returns the resolved addresses for it. Should the pinning transport land in this PR or as an immediate follow-up? (Your scope line says "UrlValidator only", so I scoped it out.)Context: I had an earlier attempt at this scope (#1117, closed once the push-specific parts landed first), and recently hardened
push_url_validator's error boundary in #1329. The draft is fully tested (26 tests; full suite green; push behavior byte-for-byte identical).Happy to re-shape to whatever scope you prefer.
refactor(utils): polish UrlValidator API and matching semantics
feat(utils): add composable UrlValidator for URL screening
refactor(utils): polish UrlValidator API and matching semantics