Skip to content

Harden outbound webhook delivery destinations and response handling - #3239

Merged
nicoloboschi merged 1 commit into
mainfrom
fix/webhook-ssrf-hardening
Aug 7, 2026
Merged

Harden outbound webhook delivery destinations and response handling#3239
nicoloboschi merged 1 commit into
mainfrom
fix/webhook-ssrf-hardening

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

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 httpx transport that:

  • resolves the host and rejects any resolved address in a blocked range,
  • pins the connection to a validated IP (preserving the Host header and
    TLS SNI) so a DNS name cannot be rebound to an internal address between
    validation and connect, and
  • tries each validated address so dual-stack hosts still connect.

Operators re-permit specific internal destinations via
HINDSIGHT_API_WEBHOOK_ALLOWED_HOSTS (hosts or IP/CIDR — e.g. 127.0.0.1 for
local testing). Registration also validates the URL for immediate 400
feedback 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 body
    gating (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

  • No request/response schema fields or endpoints changed → OpenAPI spec and
    generated clients are unaffected.
  • Docs + .env.example (and the bundled embed copy) updated.

@nicoloboschi
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
nicoloboschi force-pushed the fix/webhook-ssrf-hardening branch from 88cd9e5 to 3fc1063 Compare August 7, 2026 11:04
@nicoloboschi
nicoloboschi merged commit ad85aff into main Aug 7, 2026
104 checks passed
@nicoloboschi
nicoloboschi deleted the fix/webhook-ssrf-hardening branch August 7, 2026 11:23
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