Harden outbound webhook delivery destinations and response handling - #3239
Merged
Conversation
nicoloboschi
marked this pull request as ready for review
August 7, 2026 11:02
Webhook destination URLs are caller-supplied. Restrict where the delivery worker will connect and what it returns to callers: - Block private, loopback, and link-local destinations (incl. the cloud metadata address) by default. Operators re-permit specific hosts/CIDRs via HINDSIGHT_API_WEBHOOK_ALLOWED_HOSTS (e.g. 127.0.0.1 for local testing). - Route all delivery traffic through a guarded httpx transport that resolves the host, rejects disallowed addresses, and pins the connection to a validated IP (preserving Host + TLS SNI) so a DNS name cannot be rebound to an internal address between validation and connect. - Validate destinations at registration time for immediate 4xx feedback. - Stop returning the raw upstream response body from the delivery-history API by default; the status code is still returned. Operators can opt in with HINDSIGHT_API_WEBHOOK_EXPOSE_RESPONSE_BODY. Both flags are server-level only (not per-bank configurable). Adds unit + transport tests for the URL guard and API-layer body gating, plus HTTP integration tests for registration rejection and delivery-history gating.
nicoloboschi
force-pushed
the
fix/webhook-ssrf-hardening
branch
from
August 7, 2026 11:04
88cd9e5 to
3fc1063
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens outbound webhook delivery, where the destination URL (and method,
headers, params) is fully caller-supplied. Two controls:
1. Destination restriction (deny-by-default). The delivery worker refuses to
connect to private, loopback, or link-local addresses — including the cloud
metadata address
169.254.169.254,127.0.0.1, and RFC-1918/CGNAT/ULA ranges.All delivery traffic flows through a single guarded
httpxtransport that:Hostheader andTLS SNI) so a DNS name cannot be rebound to an internal address between
validation and connect, and
Operators re-permit specific internal destinations via
HINDSIGHT_API_WEBHOOK_ALLOWED_HOSTS(hosts or IP/CIDR — e.g.127.0.0.1forlocal testing). Registration also validates the URL for immediate
400feedback on obvious cases.
2. Response-body handling. The delivery-history API no longer returns the
raw upstream response body by default — only the status code, which is what
delivery debugging actually needs. Operators can opt in with
HINDSIGHT_API_WEBHOOK_EXPOSE_RESPONSE_BODY.Both flags are server-level only (not per-bank configurable), so a tenant
cannot re-open the ranges or re-enable body exposure for itself.
Behavior change (breaking)
Webhooks pointed at loopback/private hosts now fail unless the host is added to
HINDSIGHT_API_WEBHOOK_ALLOWED_HOSTS. This is intentional.Tests
tests/test_webhook_url_guard.py— range classification, URL validation,resolve-and-pin, guarded-transport pinning against a real loopback server
(incl. dual-stack fallback and
Host-preservation), and API-layer bodygating (default-hidden / opt-in) + a guard that the flags stay non-configurable.
tests/test_webhooks.py— HTTP integration: registration rejection (400)for internal/invalid URLs, and the delivery-history endpoint hiding the body
by default / exposing it when opted in.
Notes
generated clients are unaffected.
.env.example(and the bundled embed copy) updated.