1package webhook23import (4 "errors"5 "fmt"6 "net"7 "net/url"8 "slices"9 "strings"10)1112var (13 // ErrInvalidScheme is returned when the webhook URL scheme is not http or https.14 ErrInvalidScheme = errors.New("webhook URL must use http or https scheme")15 // ErrPrivateIP is returned when the webhook URL resolves to a private IP address.16 ErrPrivateIP = errors.New("webhook URL cannot resolve to private or internal IP addresses")17 // ErrInvalidURL is returned when the webhook URL is invalid.18 ErrInvalidURL = errors.New("invalid webhook URL")19)2021// ValidateWebhookURL validates that a webhook URL is safe to use.22// It checks:23// - URL is properly formatted24// - Scheme is http or https25// - Hostname does not resolve to private/internal IP addresses26// - Hostname is not localhost or similar.27func ValidateWebhookURL(rawURL string) error {28 if rawURL == "" {29 return ErrInvalidURL30 }3132 // Parse the URL33 u, err := url.Parse(rawURL)34 if err != nil {35 return fmt.Errorf("%w: %v", ErrInvalidURL, err)36 }3738 // Check scheme39 if u.Scheme != "http" && u.Scheme != "https" {40 return ErrInvalidScheme41 }4243 // Extract hostname (without port)44 hostname := u.Hostname()45 if hostname == "" {46 return fmt.Errorf("%w: missing hostname", ErrInvalidURL)47 }4849 // Check for localhost variations50 if isLocalhost(hostname) {51 return ErrPrivateIP52 }5354 // If it's an IP address, validate it directly55 if ip := net.ParseIP(hostname); ip != nil {56 if isPrivateOrInternalIP(ip) {57 return ErrPrivateIP58 }59 return nil60 }6162 // Resolve hostname to IP addresses63 ips, err := net.LookupIP(hostname)64 if err != nil {65 return fmt.Errorf("%w: cannot resolve hostname: %v", ErrInvalidURL, err)66 }6768 // Check all resolved IPs69 if slices.ContainsFunc(ips, isPrivateOrInternalIP) {70 return ErrPrivateIP71 }7273 return nil74}7576// isLocalhost checks if the hostname is localhost or similar.77func isLocalhost(hostname string) bool {78 hostname = strings.ToLower(hostname)79 return hostname == "localhost" ||80 hostname == "localhost.localdomain" ||81 strings.HasSuffix(hostname, ".localhost")82}8384// isPrivateOrInternalIP checks if an IP address is private, internal, or reserved.85func isPrivateOrInternalIP(ip net.IP) bool {86 // Loopback addresses (127.0.0.0/8, ::1)87 if ip.IsLoopback() {88 return true89 }9091 // Link-local addresses (169.254.0.0/16, fe80::/10)92 // This blocks AWS/GCP/Azure metadata services93 if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {94 return true95 }9697 // Private addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)98 if ip.IsPrivate() {99 return true100 }101102 // Unspecified addresses (0.0.0.0, ::)103 if ip.IsUnspecified() {104 return true105 }106107 // Multicast addresses108 if ip.IsMulticast() {109 return true110 }111112 // Additional checks for IPv4113 if ip4 := ip.To4(); ip4 != nil {114 // 0.0.0.0/8 (current network)115 if ip4[0] == 0 {116 return true117 }118 // 100.64.0.0/10 (Shared Address Space)119 if ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127 {120 return true121 }122 // 192.0.0.0/24 (IETF Protocol Assignments)123 if ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 0 {124 return true125 }126 // 192.0.2.0/24 (TEST-NET-1)127 if ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 2 {128 return true129 }130 // 198.18.0.0/15 (benchmarking)131 if ip4[0] == 198 && (ip4[1] == 18 || ip4[1] == 19) {132 return true133 }134 // 198.51.100.0/24 (TEST-NET-2)135 if ip4[0] == 198 && ip4[1] == 51 && ip4[2] == 100 {136 return true137 }138 // 203.0.113.0/24 (TEST-NET-3)139 if ip4[0] == 203 && ip4[1] == 0 && ip4[2] == 113 {140 return true141 }142 // 224.0.0.0/4 (Multicast - already handled by IsMulticast)143 // 240.0.0.0/4 (Reserved for future use)144 if ip4[0] >= 240 {145 return true146 }147 // 255.255.255.255/32 (Broadcast)148 if ip4[0] == 255 && ip4[1] == 255 && ip4[2] == 255 && ip4[3] == 255 {149 return true150 }151 }152153 return false154}155156// ValidateIPBeforeDial validates an IP address before establishing a connection.157// This is used to prevent DNS rebinding attacks.158func ValidateIPBeforeDial(ip net.IP) error {159 if isPrivateOrInternalIP(ip) {160 return ErrPrivateIP161 }162 return nil163}