channel_monitor_ssrf.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package service
  2. import (
  3. "context"
  4. "net"
  5. "strings"
  6. )
  7. // SSRF 防护 helper:
  8. // - validateEndpoint 在 admin 提交时阻止 http/loopback/私网/云元数据 URL
  9. // - safeDialContext 在 socket 层再次校验真实 IP,防止 DNS rebinding
  10. //
  11. // 已知 cloud metadata hostname 拒绝列表(小写比较)。
  12. var monitorBlockedHostnames = map[string]struct{}{
  13. "localhost": {},
  14. "localhost.localdomain": {},
  15. "metadata": {},
  16. "metadata.google.internal": {},
  17. "metadata.goog": {},
  18. "instance-data": {},
  19. "instance-data.ec2.internal": {},
  20. }
  21. // CIDR 列表:包含所有需要拒绝的 IPv4/IPv6 段。
  22. // 解析时只 panic 一次(启动时确认),生产路径只做 Contains。
  23. var monitorBlockedCIDRs = mustParseCIDRs([]string{
  24. "127.0.0.0/8", // IPv4 loopback
  25. "10.0.0.0/8", // RFC1918
  26. "172.16.0.0/12", // RFC1918
  27. "192.168.0.0/16", // RFC1918
  28. "169.254.0.0/16", // link-local(含云元数据 169.254.169.254)
  29. "100.64.0.0/10", // CGNAT
  30. "0.0.0.0/8", // "this network"
  31. "::1/128", // IPv6 loopback
  32. "fc00::/7", // IPv6 ULA
  33. "fe80::/10", // IPv6 link-local
  34. "::/128", // IPv6 unspecified
  35. })
  36. // monitorDialer 共享 Dialer,与 net/http 默认值对齐。
  37. var monitorDialer = &net.Dialer{
  38. Timeout: monitorDialTimeout,
  39. KeepAlive: monitorDialKeepAlive,
  40. }
  41. // mustParseCIDRs 在包初始化时解析 CIDR 字符串,失败 panic。
  42. func mustParseCIDRs(cidrs []string) []*net.IPNet {
  43. out := make([]*net.IPNet, 0, len(cidrs))
  44. for _, c := range cidrs {
  45. _, n, err := net.ParseCIDR(c)
  46. if err != nil {
  47. panic("channel_monitor_ssrf: invalid CIDR " + c + ": " + err.Error())
  48. }
  49. out = append(out, n)
  50. }
  51. return out
  52. }
  53. // isBlockedHostname 判断 hostname 是否命中黑名单。
  54. func isBlockedHostname(hostname string) bool {
  55. if hostname == "" {
  56. return true
  57. }
  58. _, blocked := monitorBlockedHostnames[strings.ToLower(hostname)]
  59. return blocked
  60. }
  61. // isPrivateIP 判断 IP 是否落在禁止段(loopback/RFC1918/link-local/ULA 等)。
  62. func isPrivateIP(ip net.IP) bool {
  63. if ip == nil {
  64. return true
  65. }
  66. if ip.IsUnspecified() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() {
  67. return true
  68. }
  69. for _, n := range monitorBlockedCIDRs {
  70. if n.Contains(ip) {
  71. return true
  72. }
  73. }
  74. return false
  75. }
  76. // isPrivateOrLoopbackHost 解析 hostname 的所有 A/AAAA 记录,
  77. // 任一 IP 落在私网/loopback 段即认为不安全。
  78. //
  79. // hostname 是 IP 字面量时也走同一路径。
  80. func isPrivateOrLoopbackHost(ctx context.Context, hostname string) (bool, error) {
  81. if isBlockedHostname(hostname) {
  82. return true, nil
  83. }
  84. // IP 字面量直接判断。
  85. if ip := net.ParseIP(hostname); ip != nil {
  86. return isPrivateIP(ip), nil
  87. }
  88. resolver := net.DefaultResolver
  89. addrs, err := resolver.LookupIPAddr(ctx, hostname)
  90. if err != nil {
  91. return false, err
  92. }
  93. if len(addrs) == 0 {
  94. return true, nil
  95. }
  96. for _, a := range addrs {
  97. if isPrivateIP(a.IP) {
  98. return true, nil
  99. }
  100. }
  101. return false, nil
  102. }
  103. // safeDialContext 在真实 dial 前再次校验目标 IP,防止 DNS rebinding。
  104. // 解析 hostname 后逐个 IP 尝试连接,命中私网即拒绝(即便 validateEndpoint 时返回的是公网 IP)。
  105. func safeDialContext(ctx context.Context, network, address string) (net.Conn, error) {
  106. host, port, err := net.SplitHostPort(address)
  107. if err != nil {
  108. return nil, err
  109. }
  110. // 字面量 IP 走快速路径。
  111. if ip := net.ParseIP(host); ip != nil {
  112. if isPrivateIP(ip) {
  113. return nil, &net.AddrError{Err: "blocked by SSRF policy", Addr: address}
  114. }
  115. return monitorDialer.DialContext(ctx, network, address)
  116. }
  117. if isBlockedHostname(host) {
  118. return nil, &net.AddrError{Err: "blocked by SSRF policy", Addr: address}
  119. }
  120. addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
  121. if err != nil {
  122. return nil, err
  123. }
  124. if len(addrs) == 0 {
  125. return nil, &net.AddrError{Err: "no addresses for host", Addr: host}
  126. }
  127. var lastErr error
  128. for _, a := range addrs {
  129. if isPrivateIP(a.IP) {
  130. lastErr = &net.AddrError{Err: "blocked by SSRF policy", Addr: a.IP.String()}
  131. continue
  132. }
  133. conn, err := monitorDialer.DialContext(ctx, network, net.JoinHostPort(a.IP.String(), port))
  134. if err == nil {
  135. return conn, nil
  136. }
  137. lastErr = err
  138. }
  139. if lastErr == nil {
  140. lastErr = &net.AddrError{Err: "no usable addresses", Addr: host}
  141. }
  142. return nil, lastErr
  143. }