Add default probe prompts and SSRF validation for channel monitor targets, with focused tests for blocked internal addresses.
124 lines
2.7 KiB
Go
124 lines
2.7 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var channelMonitorBlockedIPNets = mustParseChannelMonitorCIDRs([]string{
|
|
"0.0.0.0/8",
|
|
"10.0.0.0/8",
|
|
"100.64.0.0/10",
|
|
"127.0.0.0/8",
|
|
"169.254.0.0/16",
|
|
"172.16.0.0/12",
|
|
"192.0.0.0/24",
|
|
"192.0.2.0/24",
|
|
"192.168.0.0/16",
|
|
"198.18.0.0/15",
|
|
"198.51.100.0/24",
|
|
"203.0.113.0/24",
|
|
"224.0.0.0/4",
|
|
"240.0.0.0/4",
|
|
"255.255.255.255/32",
|
|
"::/128",
|
|
"::1/128",
|
|
"64:ff9b::/96",
|
|
"100::/64",
|
|
"2001::/23",
|
|
"2001:db8::/32",
|
|
"fc00::/7",
|
|
"fe80::/10",
|
|
"ff00::/8",
|
|
})
|
|
|
|
func ValidateChannelMonitorTargetURL(target string) error {
|
|
target = strings.TrimSpace(target)
|
|
if target == "" {
|
|
return fmt.Errorf("empty URL")
|
|
}
|
|
|
|
parsedURL, err := url.Parse(target)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid URL: %w", err)
|
|
}
|
|
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
|
return fmt.Errorf("only http and https allowed")
|
|
}
|
|
|
|
host := parsedURL.Hostname()
|
|
if host == "" {
|
|
return fmt.Errorf("empty host")
|
|
}
|
|
if strings.ContainsAny(host, " \t\r\n") {
|
|
return fmt.Errorf("invalid host: contains whitespace")
|
|
}
|
|
if port := parsedURL.Port(); port != "" {
|
|
portNumber, err := strconv.Atoi(port)
|
|
if err != nil || portNumber < 1 || portNumber > 65535 {
|
|
return fmt.Errorf("invalid port: %s", port)
|
|
}
|
|
}
|
|
|
|
ips, err := resolveChannelMonitorTargetHost(host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, ip := range ips {
|
|
if isBlockedChannelMonitorTargetIP(ip) {
|
|
return fmt.Errorf("private/internal IP not allowed: %s resolves to %s", host, ip.String())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resolveChannelMonitorTargetHost(host string) ([]net.IP, error) {
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return []net.IP{ip}, nil
|
|
}
|
|
|
|
ips, err := net.LookupIP(host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("DNS lookup failed for %s: %w", host, err)
|
|
}
|
|
if len(ips) == 0 {
|
|
return nil, fmt.Errorf("no IP resolved for host: %s", host)
|
|
}
|
|
return ips, nil
|
|
}
|
|
|
|
func isBlockedChannelMonitorTargetIP(ip net.IP) bool {
|
|
if ip == nil {
|
|
return true
|
|
}
|
|
if ip.IsUnspecified() || ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() {
|
|
return true
|
|
}
|
|
|
|
checkIP := ip
|
|
if ipv4 := ip.To4(); ipv4 != nil {
|
|
checkIP = ipv4
|
|
}
|
|
for _, blockedNet := range channelMonitorBlockedIPNets {
|
|
if blockedNet.Contains(checkIP) || blockedNet.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mustParseChannelMonitorCIDRs(cidrs []string) []net.IPNet {
|
|
nets := make([]net.IPNet, 0, len(cidrs))
|
|
for _, cidr := range cidrs {
|
|
_, parsedNet, err := net.ParseCIDR(cidr)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("invalid channel monitor SSRF CIDR %s: %v", cidr, err))
|
|
}
|
|
nets = append(nets, *parsedNet)
|
|
}
|
|
return nets
|
|
}
|