Skip to content

fix(webhooks): block SSRF targets on outbound delivery#1332

Merged
iammukeshm merged 2 commits into
fullstackhero:mainfrom
marcelo-maciel:fix/webhooks-ssrf-egress-guard
Jul 13, 2026
Merged

fix(webhooks): block SSRF targets on outbound delivery#1332
iammukeshm merged 2 commits into
fullstackhero:mainfrom
marcelo-maciel:fix/webhooks-ssrf-egress-guard

Conversation

@marcelo-maciel

Copy link
Copy Markdown
Contributor

Problem

Webhook subscriptions accept a tenant-supplied destination URL, so the target is untrusted input. The create validator (CreateWebhookSubscriptionCommandValidator) only enforced absolute URI + http/https scheme, and both delivery sinks — WebhookDeliveryService and WebhookDispatchJob — POSTed to new Uri(subscription.Url) with AllowAutoRedirect=true and no address screening.

Any authenticated tenant holding the ordinary Webhooks.Create permission could register http://169.254.169.254/… (cloud instance metadata), http://127.0.0.1/…, or an RFC1918 host. The delivery log records the status code / error, turning it into a blind-SSRF oracle.

Fix

  • WebhookUrlGuard — one place that screens loopback, link-local (169.254/16, cloud metadata), RFC1918, carrier-grade NAT (100.64/10), IPv6 loopback/link-local/unique-local, and localhost.
  • Create boundary — the validator rejects a blocked host, giving the tenant fast feedback.
  • Delivery boundary (authoritative) — the Webhooks named HttpClient uses a SocketsHttpHandler with AllowAutoRedirect=false and a ConnectCallback that screens the resolved IP at connect time. This closes the DNS-rebinding window a create-time hostname check leaves open, and a single handler covers both sinks (they share the named client).

Tests

CreateWebhookSubscriptionSsrfValidatorTests — the four SSRF URLs are rejected, a public HTTPS URL passes, and IsBlockedAddress is covered across IPv4/IPv6 ranges. Full Webhooks.Tests suite green (69/69).

Notes

Docs changelog entry to follow in the docs repo.

Webhook subscriptions take a tenant-supplied destination URL, so the target
is untrusted input. The create validator only checked "absolute URI + http/https
scheme", and both delivery sinks (WebhookDeliveryService, WebhookDispatchJob)
POSTed to new Uri(url) with AllowAutoRedirect=true and no address screening. Any
authenticated tenant with Webhooks.Create could register 169.254.169.254,
loopback, or an RFC1918 host and use the delivery log as a blind-SSRF oracle.

- WebhookUrlGuard: central screen for loopback / link-local (cloud metadata) /
  RFC1918 / CGNAT / IPv6 ULA, plus "localhost".
- Validator rejects blocked hosts at the create boundary (fast feedback).
- "Webhooks" HttpClient: SocketsHttpHandler with AllowAutoRedirect=false and a
  ConnectCallback that screens the resolved IP at connect time, closing the
  DNS-rebinding window left by a create-time hostname check. One handler guards
  both sinks (shared named client).
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…ailure test

The dispatch-job test created a subscription pointing at https://127.0.0.1:1 to
simulate an unreachable endpoint, but the new SSRF create guard correctly rejects
loopback targets, so the create returned 400. Switch to a .invalid host, which never
resolves (fast transient failure at DNS) and passes the create guard like any public
hostname — same test intent, compatible with the egress guard.
@iammukeshm iammukeshm merged commit a2e3d55 into fullstackhero:main Jul 13, 2026
16 checks passed
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.

2 participants