| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- package service
- import (
- "context"
- "net"
- "strings"
- )
- // SSRF 防护 helper:
- // - validateEndpoint 在 admin 提交时阻止 http/loopback/私网/云元数据 URL
- // - safeDialContext 在 socket 层再次校验真实 IP,防止 DNS rebinding
- //
- // 已知 cloud metadata hostname 拒绝列表(小写比较)。
- var monitorBlockedHostnames = map[string]struct{}{
- "localhost": {},
- "localhost.localdomain": {},
- "metadata": {},
- "metadata.google.internal": {},
- "metadata.goog": {},
- "instance-data": {},
- "instance-data.ec2.internal": {},
- }
- // CIDR 列表:包含所有需要拒绝的 IPv4/IPv6 段。
- // 解析时只 panic 一次(启动时确认),生产路径只做 Contains。
- var monitorBlockedCIDRs = mustParseCIDRs([]string{
- "127.0.0.0/8", // IPv4 loopback
- "10.0.0.0/8", // RFC1918
- "172.16.0.0/12", // RFC1918
- "192.168.0.0/16", // RFC1918
- "169.254.0.0/16", // link-local(含云元数据 169.254.169.254)
- "100.64.0.0/10", // CGNAT
- "0.0.0.0/8", // "this network"
- "::1/128", // IPv6 loopback
- "fc00::/7", // IPv6 ULA
- "fe80::/10", // IPv6 link-local
- "::/128", // IPv6 unspecified
- })
- // monitorDialer 共享 Dialer,与 net/http 默认值对齐。
- var monitorDialer = &net.Dialer{
- Timeout: monitorDialTimeout,
- KeepAlive: monitorDialKeepAlive,
- }
- // mustParseCIDRs 在包初始化时解析 CIDR 字符串,失败 panic。
- func mustParseCIDRs(cidrs []string) []*net.IPNet {
- out := make([]*net.IPNet, 0, len(cidrs))
- for _, c := range cidrs {
- _, n, err := net.ParseCIDR(c)
- if err != nil {
- panic("channel_monitor_ssrf: invalid CIDR " + c + ": " + err.Error())
- }
- out = append(out, n)
- }
- return out
- }
- // isBlockedHostname 判断 hostname 是否命中黑名单。
- func isBlockedHostname(hostname string) bool {
- if hostname == "" {
- return true
- }
- _, blocked := monitorBlockedHostnames[strings.ToLower(hostname)]
- return blocked
- }
- // isPrivateIP 判断 IP 是否落在禁止段(loopback/RFC1918/link-local/ULA 等)。
- func isPrivateIP(ip net.IP) bool {
- if ip == nil {
- return true
- }
- if ip.IsUnspecified() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() {
- return true
- }
- for _, n := range monitorBlockedCIDRs {
- if n.Contains(ip) {
- return true
- }
- }
- return false
- }
- // isPrivateOrLoopbackHost 解析 hostname 的所有 A/AAAA 记录,
- // 任一 IP 落在私网/loopback 段即认为不安全。
- //
- // hostname 是 IP 字面量时也走同一路径。
- func isPrivateOrLoopbackHost(ctx context.Context, hostname string) (bool, error) {
- if isBlockedHostname(hostname) {
- return true, nil
- }
- // IP 字面量直接判断。
- if ip := net.ParseIP(hostname); ip != nil {
- return isPrivateIP(ip), nil
- }
- resolver := net.DefaultResolver
- addrs, err := resolver.LookupIPAddr(ctx, hostname)
- if err != nil {
- return false, err
- }
- if len(addrs) == 0 {
- return true, nil
- }
- for _, a := range addrs {
- if isPrivateIP(a.IP) {
- return true, nil
- }
- }
- return false, nil
- }
- // safeDialContext 在真实 dial 前再次校验目标 IP,防止 DNS rebinding。
- // 解析 hostname 后逐个 IP 尝试连接,命中私网即拒绝(即便 validateEndpoint 时返回的是公网 IP)。
- func safeDialContext(ctx context.Context, network, address string) (net.Conn, error) {
- host, port, err := net.SplitHostPort(address)
- if err != nil {
- return nil, err
- }
- // 字面量 IP 走快速路径。
- if ip := net.ParseIP(host); ip != nil {
- if isPrivateIP(ip) {
- return nil, &net.AddrError{Err: "blocked by SSRF policy", Addr: address}
- }
- return monitorDialer.DialContext(ctx, network, address)
- }
- if isBlockedHostname(host) {
- return nil, &net.AddrError{Err: "blocked by SSRF policy", Addr: address}
- }
- addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
- if err != nil {
- return nil, err
- }
- if len(addrs) == 0 {
- return nil, &net.AddrError{Err: "no addresses for host", Addr: host}
- }
- var lastErr error
- for _, a := range addrs {
- if isPrivateIP(a.IP) {
- lastErr = &net.AddrError{Err: "blocked by SSRF policy", Addr: a.IP.String()}
- continue
- }
- conn, err := monitorDialer.DialContext(ctx, network, net.JoinHostPort(a.IP.String(), port))
- if err == nil {
- return conn, nil
- }
- lastErr = err
- }
- if lastErr == nil {
- lastErr = &net.AddrError{Err: "no usable addresses", Addr: host}
- }
- return nil, lastErr
- }
|