SEC-07: Strengthened ValidateWebhookURL to require HTTPS only (was allowing HTTP). Private IP ranges, localhost, and cloud metadata endpoints remain blocked.
100 lines
2.4 KiB
Go
100 lines
2.4 KiB
Go
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
|
|
}
|