new-api/service/channel_monitor_ssrf.go
zizi a63300fc1f feat: add channel monitor safety helpers
Add default probe prompts and SSRF validation for channel monitor targets, with focused tests for blocked internal addresses.
2026-05-20 13:28:58 +08:00

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
}