
本文介绍在 Go 中检测 URL 是否解析为私有 IP 地址(如 RFC1918、IPv6 唯一本地/链路本地/环回地址)的高效方法,支持无缝集成到 http.Client 的请求流程中,避免 SSRF 风险。
本文介绍在 go 中检测 url 是否解析为私有 ip 地址(如 rfc1918、ipv6 唯一本地/链路本地/环回地址)的高效方法,支持无缝集成到 `http.client` 的请求流程中,避免 ssrf 风险。
在构建接受用户输入 URL 并发起 HTTP 请求的服务(如 Webhook 代理、URL 预览服务或反向代理)时,必须防范服务器端请求伪造(SSRF)攻击。攻击者若传入 http://10.0.1.5/admin/api 或 http://[fd00::1]/metrics 等指向内网资源的 URL,可能导致敏感服务暴露或内网探测。因此,在执行 http.Get() 前,必须对目标主机的解析 IP 进行私有地址校验。
Go 标准库未提供开箱即用的“是否私有 IP”判断函数,但可通过预定义 CIDR 网段 + net.IPNet.Contains() 实现高可靠检测。以下方案覆盖全部主流私有与受限地址空间:
- ✅ IPv4:
127.0.0.0/8(环回)、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16(RFC1918)、169.254.0.0/16(链路本地,RFC3927) - ✅ IPv6:
::1/128(环回)、fe80::/10(链路本地)、fc00::/7(唯一本地地址,ULA,RFC4193) - ✅ 补充:
ip.IsLoopback()、ip.IsLinkLocalUnicast()等内置方法可覆盖部分边界情况,提升鲁棒性
完整实现如下:
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 addr
} {
_, 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 {
// 快速路径:利用标准库内置判断(覆盖常见场景)
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
return true
}
// 归一化:处理 IPv4-mapped IPv6 地址(如 ::ffff:192.168.1.1)
if v4 := ip.To4(); v4 != nil {
ip = v4
}
// 逐网段匹配
for _, block := range privateIPBlocks {
if block.Contains(ip) {
return true
}
}
return false
}
// 安全的 HTTP GET 封装:自动校验目标 IP
func safeHTTPGet(u string) (*http.Response, error) {
urlObj, err := url.Parse(u)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
host, port, _ := net.SplitHostPort(urlObj.Host)
if host == "" {
host = urlObj.Host // 无端口时 Host 字段已含域名/IP
}
// 解析主机为 IP(仅 IPv4/IPv6,不触发 DNS 重定向)
ips, err := net.LookupHost(host)
if err != nil {
return nil, fmt.Errorf("DNS resolution failed for %s: %w", host, err)
}
for _, ipStr := range ips {
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
if isPrivateIP(ip) {
return nil, fmt.Errorf("refusing request to private IP address: %s (resolved from %s)", ipStr, host)
}
}
// 所有解析出的 IP 均通过校验,执行请求
return http.Get(u)
}⚠️ 关键注意事项:
-
不要仅依赖
net.URL.Host字符串匹配(如正则匹配"10."),IP 地址可能以 IPv6 形式([2001:db8::1])或十六进制形式出现; -
务必调用
ip.To4()处理 IPv4-mapped IPv6 地址(如::ffff:192.168.1.1),否则net.IPNet.Contains()可能因版本不匹配返回false; - 若需深度集成至
http.Client,可自定义http.Transport.DialContext,在连接建立前注入 IP 校验逻辑,避免重复解析; - 生产环境建议结合
net/http/httputil.ReverseProxy的Director函数,在代理转发前完成校验; - 对于 DNS 重绑定(DNS rebinding)攻击,单次解析仍不足,需在每次连接时重新解析并校验(代价较高,按需启用)。
该方案兼顾安全性、性能与标准兼容性,是 Go 生态中防御 SSRF 的推荐实践。

















