package validators import ( "fmt" "net" "net/url" "strings" ) // blockedHostnames contains hostnames that must never be used as webhook targets (SSRF protection). var blockedHostnames = []string{ "localhost", "127.0.0.1", "metadata", "metadata.google.internal", "metadata.google", "instance-data", "169.254.169.254", } // IsInternalOrPrivateURL returns true if the URL targets an internal or private address (SSRF risk). // Blocks: localhost, 127.0.0.1, 10.x, 172.16-31.x, 192.168.x, 169.254.x (cloud metadata). func IsInternalOrPrivateURL(rawURL string) bool { parsed, err := url.Parse(rawURL) if err != nil { return true // Invalid URL treated as unsafe } scheme := strings.ToLower(parsed.Scheme) if scheme != "http" && scheme != "https" { return true } hostname := strings.ToLower(parsed.Hostname()) if hostname == "" { return true } for _, blocked := range blockedHostnames { if hostname == blocked { return true } } ips, err := net.LookupIP(hostname) if err != nil { return true // Resolution failure treated as unsafe } for _, ip := range ips { if isPrivateIP(ip) { return true } } return false } func isPrivateIP(ip net.IP) bool { privateRanges := []string{ "10.0.0.0/8", // RFC1918 "172.16.0.0/12", // RFC1918 "192.168.0.0/16", // RFC1918 "127.0.0.0/8", // Loopback "169.254.0.0/16", // Link-local / cloud metadata "::1/128", // IPv6 loopback "fc00::/7", // IPv6 unique local "fe80::/10", // IPv6 link-local } for _, cidrStr := range privateRanges { _, cidr, err := net.ParseCIDR(cidrStr) if err != nil { continue } if cidr.Contains(ip) { return true } } return false } // ValidateWebhookURL validates that a webhook URL is safe for registration. // SEC-07: Only HTTPS allowed. Blocks private/internal IPs (SSRF protection). func ValidateWebhookURL(rawURL string) error { parsed, err := url.Parse(rawURL) if err != nil { return fmt.Errorf("invalid URL: %w", err) } scheme := strings.ToLower(parsed.Scheme) if scheme != "https" { return fmt.Errorf("only https URLs are allowed for webhooks (got %q)", parsed.Scheme) } hostname := strings.ToLower(parsed.Hostname()) if hostname == "" { return fmt.Errorf("URL must have a hostname") } if IsInternalOrPrivateURL(rawURL) { return fmt.Errorf("URL targets internal or private address - not allowed for webhooks") } return nil }