// Webhook delivery proxy — pre-fix v3.2.0 // Internal infra service; receives webhook URLs from tenants and forwards // the body after an "allowlist" check. package webhookproxy import ( "io" "net" "net/http" "net/url" "strings" ) var internalCIDRs = []string{"10.0.0.0/8", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"} func isInternalIP(addr string) bool { ip := net.ParseIP(addr) for _, c := range internalCIDRs { _, n, _ := net.ParseCIDR(c) if n.Contains(ip) { return true } } return false } // validateTarget resolves the hostname ONCE and checks against the deny list. // Returns nil if safe. func validateTarget(rawurl string) error { u, err := url.Parse(rawurl) if err != nil { return err } if u.Scheme != "http" && u.Scheme != "https" { return ErrBadScheme } host := u.Hostname() ips, err := net.LookupIP(host) if err != nil { return err } for _, ip := range ips { if isInternalIP(ip.String()) { return ErrInternalTarget } } return nil } func DeliverWebhook(tenant string, rawurl string, body []byte) (int, error) { if err := validateTarget(rawurl); err != nil { return 0, err } // Build request and send. Note: net/http will perform ITS OWN DNS lookup // when dialing the URL — independent of validateTarget's earlier lookup. req, _ := http.NewRequest("POST", rawurl, strings.NewReader(string(body))) req.Header.Set("X-Webhook-Tenant", tenant) resp, err := http.DefaultClient.Do(req) if err != nil { return 0, err } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) return resp.StatusCode, nil }