ValidateEndpointURL checks the webhook endpoint URL for SSRF. Rejects private/loopback/link-local IPs and non-http(s) schemes.
(endpoint string)
| 9 | // ValidateEndpointURL checks the webhook endpoint URL for SSRF. |
| 10 | // Rejects private/loopback/link-local IPs and non-http(s) schemes. |
| 11 | func ValidateEndpointURL(endpoint string) error { |
| 12 | u, err := url.Parse(endpoint) |
| 13 | if err != nil { |
| 14 | return fmt.Errorf("malformed URL") |
| 15 | } |
| 16 | if u.Scheme != "http" && u.Scheme != "https" { |
| 17 | return fmt.Errorf("only http and https schemes are allowed") |
| 18 | } |
| 19 | host := u.Hostname() |
| 20 | if host == "" { |
| 21 | return fmt.Errorf("missing host") |
| 22 | } |
| 23 | |
| 24 | // Resolve the hostname to IP addresses |
| 25 | ips, err := net.LookupHost(host) |
| 26 | if err != nil { |
| 27 | return fmt.Errorf("failed to resolve host: %s", err.Error()) |
| 28 | } |
| 29 | |
| 30 | for _, ipStr := range ips { |
| 31 | ip := net.ParseIP(ipStr) |
| 32 | if ip == nil { |
| 33 | return fmt.Errorf("invalid IP address resolved") |
| 34 | } |
| 35 | if isPrivateIP(ip) { |
| 36 | return fmt.Errorf("requests to private/internal networks are not allowed") |
| 37 | } |
| 38 | } |
| 39 | return nil |
| 40 | } |
| 41 | |
| 42 | // isPrivateIP returns true if the IP is in a private, loopback, link-local, |
| 43 | // or otherwise non-routable range. |
no test coverage detected