Repository navigation
fix(webhooks): reject internal and metadata addresses in webhook delivery - #467
polylane[bot] wants to merge 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the
|
Deploying usesend with
|
| Latest commit: |
8ee2a7d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2ef18351.usesend.pages.dev |
| Branch Preview URL: | https://polylane-autofix-4p0jdtvo2jc.usesend.pages.dev |
| try { | ||
| await assertWebhookUrlAllowed(url); | ||
| } catch (error) { | ||
| if (error instanceof WebhookUrlNotAllowedError) { |
There was a problem hiding this comment.
Log each refused non-public webhook URL so an operator can count SSRF attempts at the save path.
| if (error instanceof WebhookUrlNotAllowedError) { | |
| if (error instanceof WebhookUrlNotAllowedError) { | |
| logger.warn( | |
| { reason: error.message }, | |
| "[WebhookService]: Rejected non-public webhook URL", | |
| ); |
|
Tip Expected to resolve the linked issue. Linked issue: Webhook delivery calls tenant URLs without an SSRF check Checked the unsend production service. 330 webhook deliveries in 72h, all HTTP 200, no failures. The guard adds one DNS resolve before the existing fetch, and CPU stayed under 0.05 vCPU of a 32 vCPU limit. Also considered · 1 plausible, 3 refuted
Analysed against 1 Service and 1 repository
Polylane analysed Rate this review: 👍 helpful · 👎 not helpful · 😕 confusing |
1358dc5 to
22d8da3
Compare
|
Follow-up commit (rebased onto
Verified: Not in this PR: SNS message signature verification for |
|
@KMKoushik I can help with this once you have access to the koushik's Workspace workspace. Already have a Polylane account? Link your GitHub account and I'll pick this up. |
…very Co-authored-by: polylane[bot] <277585245+polylane[bot]@users.noreply.github.com>
…private-address opt-out - Route webhook delivery, SNS subscription confirmation and the SES callback URL check through one shared guard (server/utils/outbound-url-guard.ts). - Check resolved addresses inside the connection's DNS lookup so a hostname cannot rebind to an internal address between check and connect; the lookup now runs under the request timeout. - Only reject at save time on private literals or private resolutions, not on lookup failures. - Add OUTBOUND_ALLOW_PRIVATE_ADDRESSES for self-hosters that deliver to internal hosts. - Block 2001::/32, 192.88.99.0/24 and fec0::/10. - Share one test HTTP server per suite.
…_ALLOW_PRIVATE_URLS Rename OUTBOUND_ALLOW_PRIVATE_ADDRESSES to WEBHOOK_ALLOW_PRIVATE_URLS and default it from NEXT_PUBLIC_IS_CLOUD: the guard is on in cloud and off on self-hosted unless the operator sets WEBHOOK_ALLOW_PRIVATE_URLS=false, so existing self-hosted webhooks to hosts like http://n8n:5678 keep delivering after upgrade. The SNS SubscribeURL and SES callback URL checks keep main's behaviour: main already pins SubscribeURL to AWS SNS hosts (#466), and the callback check is admin-only. Add tests that a hanging DNS lookup is cut short by the request signal and counted against the webhook request timeout.
22d8da3 to
8ee2a7d
Compare
Fixes: Webhook delivery calls tenant URLs without an SSRF check
Any team member could save a webhook destination on an internal address. The delivery worker sent the signed payload to that address, and the tenant could read the response back from the webhook call record. Once this merges, an internal, loopback, link-local, or metadata destination is no longer usable for a webhook.
Detection evidence
Affected:
int_13ee3c91c001c2c8xfdj06a9Why this fix
A unit test drives the real delivery worker at a loopback webhook target, and the worker contacted it, while the repository docs already require a publicly accessible endpoint. The worker now resolves the host and refuses a private, loopback, link-local, or metadata address before it sends, and the save path refuses the same addresses so the tenant gets a clear error instead of a silently failing webhook. The residual risk is a DNS answer that changes between the save check and the send check, because the resolved address is not pinned.
Fix walkthrough
Watch the fix explained (1:33). Recorded for commit
8ee2a7d184fce4383318b1c342764391be3299d3.Out of scope
SesSettingsService.createSesSettingfetches an admin-supplied URL without this guard. Its caller is the platform-admin procedure, a different boundary, and it is recorded on the incident timeline.Causal chain
4 files changed (+624/-0)
apps/web/src/server/service/webhook-service.ts: modified, +26/-0apps/web/src/server/service/webhook-service.unit.test.ts: modified, +6/-0apps/web/src/server/service/webhook-url-guard.ts: added, +250/-0apps/web/src/server/service/webhook-url-guard.unit.test.ts: added, +342/-0Repository lint:
pnpm run lint(declared in package.json) fails over the whole repository (the declared target takes no file arguments), so the failure is not attributed to this change and did not block it; run it before merging.Lint output
Generated by Polylane. You can ask follow-ups by mentioning @polylane in a comment.
Summary by cubic
Rejects webhook destinations that point at or resolve to internal, loopback, link-local, or cloud metadata addresses, closing an SSRF hole where any team member could make the delivery worker send signed payloads to tenant-controlled internal hosts.
BAD_REQUESTinstead of storing a silently failing webhook; a failed or slow DNS lookup at save time does not block saving.WEBHOOK_ALLOW_PRIVATE_URLS, which defaults on in self-hosted installs (so existing webhooks to internal hosts likehttp://n8n:5678keep delivering) and off in cloud; when off, non-public addresses are rejected.Written for commit 8ee2a7d. Summary will update on new commits.