Security [HIGH]: SSRF guard misses IPv6 private ranges + webhook dispatcher lacks DNS-rebind protection #49
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
(1)
ssrf-guard.tsblocklist covers IPv4 RFC1918 + loopback but misses IPv6 link-localfe80::/10, ULAfc00::/7, IPv4-mapped::ffff:...,0.0.0.0/8, and partial100.64.0.0/10. (2)testWebhookvalidates URL via guard, butfetchinwebhook-dispatcher.ts:117re-resolves DNS — DNS-rebinding attacks succeed (public IP at validate, private at fetch). (3)webhook-dispatcher.tsNEVER callsassertWebhookUrlAllowedbefore actual delivery, so legacy/manually-edited webhook URLs bypass guard entirely.Evidence
packages/api/src/lib/ssrf-guard.ts:11-24 — IPv4-only blocklistpackages/api/src/router/webhook-procedure-support.ts:117-138 — guard runs, then fetch re-resolvespackages/api/src/router/webhook-procedure-support.ts:72-73 — updateWebhook re-validates only if URL changesImpact
Admin-configured webhook reaches internal IPv6 services (etcd, metrics), metadata endpoints, or via DNS-rebind any private IP at dispatch time.
Proposed Fix
(1) Add IPv6 private prefix blocklist + IPv4-mapped check. (2) Use
dns.lookup({ all: true })and check every result. (3) Pin resolved IP and pass viaundiciAgentwithconnect.lookupoverride. (4) CallassertWebhookUrlAllowed(wh.url)insidedeliverWebhookright beforefetch.Acceptance Criteria
Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (B-14, B-23, B-25)
Resolved in commit
4ff7bc9(security: SSRF guard covers IPv6 + DNS-rebind defence via pinned IP).SSRF-guard (packages/api/src/lib/ssrf-guard.ts) — blocks full IPv4 private space (loopback, RFC 1918, link-local, CGNAT 100.64/10, 198.18/15 benchmark, TEST-NETs, 224/4 multicast, 240/4 reserved) and IPv6 (::1, fc00::/7 ULA, fe80::/10 link-local, ff00::/8 multicast, 2001:db8::/32, ::ffff:… mapped → v4).
dns.lookup({all:true})+ reject-if-any-private covers multi-record rebind attacks.Webhook dispatcher (packages/api/src/lib/webhook-dispatcher.ts) — uses Node native
https.requestwith a customAgent({lookup})that pins the pre-validated IP at socketconnect()time. The real hostname stays on the Host header + SNI for cert validation. A DNS rebind between guard-check and dial cannot redirect the connection.Tests: 28 cases in
packages/api/src/__tests__/ssrf-guard.test.ts(including multi-record rebind + IPv6-mapped v4), plus a DNS-rebind regression inpackages/api/src/__tests__/webhook-dispatcher.test.tsthat invokes the Agent.lookup callback and verifies the pinned IP.