
本文介绍一种高效、可集成到 http.Client 中的 Go 语言方案,用于在发起 HTTP 请求前自动检测目标 URL 解析出的 IP 是否属于私有网络(RFC1918)、回环、链路本地或唯一本地 IPv6 等不可路由地址空间,从而防止 SSRF 攻击。
本文介绍一种高效、可集成到 http.client 中的 go 语言方案,用于在发起 http 请求前自动检测目标 url 解析出的 ip 是否属于私有网络(rfc1918)、回环、链路本地或唯一本地 ipv6 等不可路由地址空间,从而防止 ssrf 攻击。
在构建对外提供 URL 接入能力的服务(如 Webhook 回调、爬虫代理、富媒体预览等)时,若直接对用户输入的 URL 执行 http.Get,极易引发服务端请求伪造(SSRF) 风险——攻击者可构造类似 http://10.0.1.5:8080/internal/api 的 URL,诱导服务端访问内网资源。因此,在 DNS 解析后、HTTP 连接建立前校验目标 IP 的网络属性,是关键的安全防线。
Go 标准库未内置“是否私有 IP”的通用判断函数,但可通过组合 net.IP 方法与 CIDR 匹配实现健壮检查。以下是一个生产就绪的实现:
package main
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
)
var privateIPBlocks []*net.IPNet
func init() {
for _, cidr := range []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", // RFC3927 link-local
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fc00::/7", // IPv6 unique local address (ULA)
} {
_, block, err := net.ParseCIDR(cidr)
if err != nil {
panic(fmt.Errorf("failed to parse CIDR %q: %v", cidr, err))
}
privateIPBlocks = append(privateIPBlocks, block)
}
}
func isPrivateIP(ip net.IP) bool {
// 利用标准库快速判断常见类型(避免遍历 CIDR)
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
// 归一化:IPv4 转为 IPv4-mapped IPv6 可统一处理,但此处更推荐显式处理 IPv4/v6
ip = ip.To16() // 确保为 16 字节格式(兼容 IPv4)
if ip == nil {
return false
}
for _, block := range privateIPBlocks {
if block.Contains(ip) {
return true
}
}
return false
}
要将该逻辑无缝集成进 http.Client,推荐使用 自定义 DialContext ——这是 http.Transport 中控制底层 TCP 连接建立的核心钩子:
func newSafeHTTPClient() *http.Client {
return &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// 解析 host:port 中的 host 部分
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("invalid address %q: %v", addr, err)
}
// DNS 解析(支持 IPv4/IPv6 双栈)
ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, fmt.Errorf("DNS resolve failed for %q: %v", host, err)
}
if len(ips) == 0 {
return nil, fmt.Errorf("no IP address returned for %q", host)
}
// 检查任一解析结果是否为私有地址(防御 DNS 劫持/多 IP 场景)
for _, ipAddr := range ips {
ip := ipAddr.IP
if isPrivateIP(ip) {
return nil, fmt.Errorf("refusing to connect to private IP %v (host: %q)", ip, host)
}
}
// 安全通过,交由默认拨号器建立连接
return (&net.Dialer{}).DialContext(ctx, network, addr)
},
},
}
}
// 使用示例
func main() {
client := newSafeHTTPClient()
u, _ := url.Parse("http://192.168.1.100:8080/status")
req, _ := http.NewRequest("GET", u.String(), nil)
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Request blocked: %v\n", err) // 输出: refusing to connect to private IP ...
return
}
defer resp.Body.Close()
}
✅ 关键优势说明:
- 零侵入集成:无需修改业务层 URL 处理逻辑,完全在 Transport 层拦截;
- 防御全面:覆盖 IPv4/IPv6 的私有网段、回环、链路本地、唯一本地地址(ULAs),符合现代网络环境;
- 精准阻断:在 TCP 连接建立前拒绝,不触发任何网络 I/O,杜绝旁路风险;
-
可扩展性强:可轻松添加
100.64.0.0/10(CGNAT)或企业内网段等自定义规则。
⚠️ 注意事项:
- 若服务需访问
localhost或容器内127.0.0.1(如 sidecar),请根据实际场景调整策略(例如白名单localhost域名); -
isPrivateIP不替代 TLS 证书校验、HTTP 重定向限制等其他 SSRF 缓解措施,应作为纵深防御的一环; - 生产环境建议配合
context.WithTimeout控制 DNS 解析耗时,避免 DoS。
通过此方案,你能在保持 Go 原生 HTTP 客户端简洁性的同时,为 URL 外部调用筑起第一道坚实的安全屏障。










