Summary
adcp.webhooks.deliver() (the deprecated AdCP 3.x HMAC-SHA256 path) constructs an unpinned httpx.AsyncClient(timeout=...) and POSTs to a buyer-controlled URL when no operator client is supplied. No SSRF guard.
Source: src/adcp/webhooks.py:867-872. Surfaced by security-reviewer on PR #297 as L4 — explicitly deferred from the prep PR scope.
Why deferred
deliver() is the AdCP 3.x deprecated path; emits DeprecationWarning already; new traffic goes through WebhookSender. PR #297 was scoped to RFC 9421 sender hardening.
Proposed fix
Mirror the WebhookSender._send_bytes pattern:
- When the operator supplies a client, trust them.
- When the sender owns the client, build a per-request
AsyncIpPinnedTransport via build_async_ip_pinned_transport(url, ...) and construct httpx.AsyncClient(transport=transport, follow_redirects=False, trust_env=False).
- Same
allow_private_destinations and allowed_destination_ports kwargs on the public surface.
Risk
Adopters on the legacy deliver() path posting to private/internal endpoints (dev/test fixtures) will start getting SSRFValidationError. One-kwarg opt-out via allow_private=True (or whatever name fits the existing deliver() surface).
References
Summary
adcp.webhooks.deliver()(the deprecated AdCP 3.x HMAC-SHA256 path) constructs an unpinnedhttpx.AsyncClient(timeout=...)and POSTs to a buyer-controlled URL when no operator client is supplied. No SSRF guard.Source:
src/adcp/webhooks.py:867-872. Surfaced by security-reviewer on PR #297 as L4 — explicitly deferred from the prep PR scope.Why deferred
deliver()is the AdCP 3.x deprecated path; emitsDeprecationWarningalready; new traffic goes throughWebhookSender. PR #297 was scoped to RFC 9421 sender hardening.Proposed fix
Mirror the
WebhookSender._send_bytespattern:AsyncIpPinnedTransportviabuild_async_ip_pinned_transport(url, ...)and constructhttpx.AsyncClient(transport=transport, follow_redirects=False, trust_env=False).allow_private_destinationsandallowed_destination_portskwargs on the public surface.Risk
Adopters on the legacy
deliver()path posting to private/internal endpoints (dev/test fixtures) will start gettingSSRFValidationError. One-kwarg opt-out viaallow_private=True(or whatever name fits the existingdeliver()surface).References
src/adcp/webhooks.py:867-872src/adcp/webhook_sender.py:_send_bytes(post PR feat(signing): close 4 SSRF gaps and add opt-in port hardening (foundation audit) #297).context/foundations-audit/FINDINGS.md