ssrf 是服务端请求伪造,go 因 net/http 默认不限制 url 而易受攻击;防护需三步:解析 url、校验协议(仅 http/https)、检查主机(禁用私有 ip 和非白名单域名)。

SSRF 是什么,为什么 Go 服务特别容易中招
SSRF(Server-Side Request Forgery)不是用户直接攻击你的接口,而是诱使你的 Go 服务自己去“访问不该访问的地方”——比如 http://127.0.0.1:8080/admin 或内网 http://10.0.1.5:3306。Go 的 net/http 默认不做任何 URL 限制,http.Get(req.URL) 一调就发,只要传进来的是合法 URL,它就真去连。而很多业务场景天然需要“代理请求”或“URL 回调校验”,比如:富文本图片自动下载、Webhook 地址验证、OAuth 第三方回调重定向地址解析……这些地方一旦对用户输入的 URL 缺乏白名单或协议/域名约束,SSRF 就几乎必然发生。
用 net/url + net/http 做基础校验的硬核写法
别依赖第三方中间件——SSRF 防护必须在业务逻辑入口做,且要细粒度控制。核心是三步:解析 URL → 检查协议 → 校验主机(含 IP 归属)。下面这段是生产可用的最小防护骨架:
func isAllowedURL(rawURL string) error {
u, err := url.Parse(rawURL)
if err != nil {
return fmt.Errorf("invalid URL format")
}
if u.Scheme != "https" && u.Scheme != "http" {
return fmt.Errorf("disallowed scheme: %s", u.Scheme)
}
// 解析 host,支持域名和 IPv4/IPv6
host, port, _ := net.SplitHostPort(u.Host)
if host == "" {
host = u.Host
}
ip := net.ParseIP(host)
if ip != nil {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() {
return fmt.Errorf("disallowed private/loopback IP: %s", host)
}
}
// 白名单域名(建议从配置加载,而非硬编码)
allowedHosts := map[string]bool{"api.example.com": true, "cdn.example.net": true}
if _, ok := allowedHosts[host]; !ok {
return fmt.Errorf("host not in allowlist: %s", host)
}
return nil
}
常见错误现象:url.Parse 后只检查 u.Host,却忽略 u.Scheme 被设为 file:// 或 ftp://;或用 strings.HasPrefix(u.Host, "127.") 这种字符串匹配,漏掉 127.0.0.1:8080 或 IPv6 的 ::1。
gorilla/handlers.CORS 不能防 SSRF,但 SameSite 和 Referer 可辅助判断
CORS 是浏览器行为,跟 SSRF 完全无关——攻击者根本不用浏览器,直接 curl -X POST 构造请求即可绕过所有前端限制。真正有用的辅助手段只有两个:Referer 头校验(需确认你允许的来源可信)和 SameSite=Strict Cookie 设置(防止跨站带认证态发起 SSRF 请求)。但注意:SameSite 对纯 API 场景无效,且不能替代 URL 白名单。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
http.SetCookie(w, &http.Cookie{Name: "session", Value: "...", SameSite: http.SameSiteStrictMode})- Referer 校验仅适用于有明确可信来源的场景,如 Webhook 回调必须来自
github.com,此时可加if r.Referer() != "https://github.com/" { http.Error(w, "bad referer", http.StatusForbidden) }
用 http.Client 自定义 Transport 阻断内网 DNS 解析
最彻底的防御是在 HTTP 客户端层面掐断内网请求。Go 允许你替换 http.DefaultClient.Transport,用自定义 RoundTripper 在 DNS 解析阶段拦截非法 IP。关键点是:不要等 DialContext 连接时才检查,而要在 Resolver 返回结果后立刻过滤。
type safeResolver struct{}
func (r safeResolver) LookupHost(ctx context.Context, host string) ([]string, error) {
ips, err := net.DefaultResolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
var allowed []string
for _, ip := range ips {
if !isPrivateIP(ip) {
allowed = append(allowed, ip)
}
}
if len(allowed) == 0 {
return nil, fmt.Errorf("all resolved IPs are private/loopback: %v", ips)
}
return allowed, nil
}
func isPrivateIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified()
}
参数差异:http.Client 必须显式设置 Transport,否则默认走 http.DefaultTransport;若用了 fasthttp 或其他 HTTP 库,这套机制不适用,得单独适配。
真正难啃的点在于:有些业务必须支持内网服务调用(比如微服务间通信),这时白名单就得动态化——要么按租户隔离域名/IP 列表,要么引入服务发现中心做运行时鉴权。这种复杂度,没法靠一个中间件解决。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










