Add an egress CIDR allowlist to the SSRF guard (closes #204)
All checks were successful
check / check (push) Successful in 5m25s

The SSRF blocklist had no escape hatch, so the thing webhooker is
mostly for — taking a public webhook and forwarding it to something
on your own network — could not be configured at all. Every private
address, Docker sibling and loopback service was permanently
unreachable as a delivery destination.

ALLOWED_EGRESS_CIDRS (default empty) names blocks that delivery
targets may reach despite the default blocklist. It is an allowlist
and only ever adds destinations: there is no boolean, and no value
disables SSRF protection wholesale. Empty, the guard behaves exactly
as before.

Link-local (169.254.0.0/16, fe80::/10) is refused before the
allowlist is consulted, so no supplied CIDR can open it — not the
exact address, not a supernet, not 0.0.0.0/0. Reaching cloud
instance metadata is credential theft rather than delivery to an
internal service.

The policy now lives in one function, Guard.checkIP, which both
target-creation validation and the delivery dialer call. The two
paths previously decided separately, which is how they came to
disagree about a destination. The guard is built once from config
and injected via fx into both the handlers and the delivery engine,
so there is a single instance and a single answer.

A set-but-unparseable value aborts startup naming the variable,
reusing the existing envPrefixList parser. A non-empty list is
logged at startup with the blocks spelled out, not counted, so the
hole is visible in the log of any deployment that has one.

Tests: an allowlisted loopback CIDR both validates and delivers to a
live server (and the same URL still fails without the allowlist); a
private address outside the listed block stays refused on both
paths; metadata stays refused under six different covering CIDRs;
public addresses are unaffected either way; and config coverage for
parsing, startup abort, and the warning's contents.
This commit is contained in:
2026-08-20 04:13:06 +00:00
parent 10c8dd2331
commit 71a3c3cf75
16 changed files with 886 additions and 61 deletions

View File

@@ -59,6 +59,7 @@ type HandlersParams struct {
Middleware *middleware.Middleware
Notifier delivery.Notifier
Evictor delivery.WebhookEvictor
SSRFGuard *delivery.Guard
}
// Handlers provides HTTP handler methods for all application
@@ -75,6 +76,11 @@ type Handlers struct {
evictor delivery.WebhookEvictor
templates map[string]*template.Template
// ssrf validates submitted target URLs. It is the same guard
// the delivery engine dials through, so a URL accepted here
// is one delivery will actually attempt.
ssrf *delivery.Guard
// dummyVerifications counts the equivalent-cost verifications
// charged for usernames that do not exist. It exists so a test
// can prove that path runs without measuring wall-clock time.
@@ -114,6 +120,7 @@ func New(
s.mw = params.Middleware
s.notifier = params.Notifier
s.evictor = params.Evictor
s.ssrf = params.SSRFGuard
// Parse all page templates once at startup
s.templates = map[string]*template.Template{