Go 回调地址怎么做 SSRF 防护:域名白名单、解析结果与跳转控制
来源:17golang原创
时间:2026-08-10 12:48:59 116浏览 收藏
图片抓取接口上线后,最容易被忽略的参数往往就是 url:业务只想下载一张封面,服务端却可能被引到内网管理端口。Go 的 http.Client 上手很好用,但它不会替你判断目标是不是可信地址。真正稳妥的做法,是在发起请求前先解析URL,限制可用协议和端口,解析域名得到所有IP后逐个校验私网范围,同时明确关闭或重新校验重定向逻辑。
防 SSRF 不是写一个“不能包含 127.0.0.1”的黑名单,而是把“允许访问哪些目标”变成可测试的策略;解析、DNS、连接和跳转这几层缺一不可。
- 只接受
https或业务确需的http,端口采用白名单机制。 - 域名不能只做字符串匹配,必须解析 A/AAAA 记录并拒绝回环、私网、链路本地和未指定地址。
- 默认禁用自动重定向;如果业务必须跟随跳转,每一跳都要重新执行同一套校验策略。
- 把解析失败、命中禁区、重定向和响应大小全部记录到可审计的日志里。
一个“远程封面”接口为什么会越权访问内网
故障现场通常很普通:前端传来一个图片地址,后端调用 http.Get,读完响应体后存入对象存储。开发者可能顺手加了一个判断:URL 里不能出现 localhost。这个简单检查挡不住数字IP、IPv6、解析到内网的域名,也挡不住一次 302 跳转。
威胁模型先理清楚三件事:攻击者能控制什么参数、服务端拥有什么网络权限、响应内容会不会直接返回给攻击者。如果服务端能访问数据库管理面、容器元数据或内部HTTP API,那么这个下载功能就是一个“带着服务器权限发请求”的隐形代理。OWASP 也把URL解析差异、协议限制、重定向和DNS解析列为需要同时覆盖的边界校验点。

先把允许访问的目标写成固定策略
不要先写黑名单再不停补漏洞。对图片抓取这类功能,我更建议先和业务确认四个问题:只允许HTTPS吗?是否只允许443端口?目标是任意公网域名,还是有限的CDN域名?是否需要跟随跳转?答案不同,代码里的策略逻辑也应该对应调整。
下面这个策略适合“抓取公网图片”的保守起点:只接受 https,显式指定的端口只能是443;如果业务必须支持 http,就把它作为单独配置项走风险评审流程,而不是默认放开。路径和查询参数交给目标站点处理,但用户不能靠修改URL来切换协议或指定高危端口。
type FetchPolicy struct {
AllowedSchemes map[string]bool
AllowedPorts map[string]bool
FollowRedirect bool
}
var publicImagePolicy = FetchPolicy{
AllowedSchemes: map[string]bool{"https": true},
AllowedPorts: map[string]bool{"443": true},
FollowRedirect: false,
}
策略对象的价值在于可审计:测试同学可以直接确认“http协议被拒绝”“显式8443端口被拒绝”“默认不跟随跳转”,而不是依赖一串散落在处理函数里的零散条件。
URL 解析之后,再检查主机和端口
url.Parse 只负责把文本格式的地址解析成结构化URL,它本身不是安全判定器。解析结果要逐层检查scheme、Host、Hostname和Port;空scheme、带用户名密码、缺少主机名、异常端口这些情况,都应该在发请求前直接终止流程。
func checkURL(raw string, policy FetchPolicy) (*url.URL, error) {
u, err := url.Parse(raw)
if err != nil {
return nil, fmt.Errorf("url parse failed: %w", err)
}
if !policy.AllowedSchemes[u.Scheme] || u.User != nil {
return nil, errors.New("scheme or user-info is not allowed")
}
host := u.Hostname()
if host == "" {
return nil, errors.New("host is required")
}
port := u.Port()
if port == "" {
port = defaultPort(u.Scheme)
}
if !policy.AllowedPorts[port] {
return nil, errors.New("port is not allowed")
}
return u, nil
}
func defaultPort(scheme string) string {
if scheme == "https" {
return "443"
}
return "80"
}
这里不要用字符串拼接的方式去判断 u.Host。比如带方括号的IPv6、大小写不同的主机名和附带显式端口的情况,只有先交给标准解析器处理,后续的检查逻辑才能拿到稳定的输入。
DNS 解析是第二道门:所有结果都要过 IP 校验策略
域名看起来是公网域名,不代表解析结果永远是公网地址。服务端应该拿到A和AAAA解析结果,使用 net/netip 逐个校验每个地址,拒绝回环、私网、链路本地、未指定地址以及业务明确不允许的保留网段。解析失败的时候不能降级成“直接继续发起请求”。
func rejectNonPublic(ctx context.Context, host string) error {
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
if err != nil || len(ips) == 0 {
return errors.New("host cannot be resolved")
}
for _, addr := range ips {
if !isPublicTarget(addr) {
return fmt.Errorf("resolved address is not allowed: %s", addr)
}
}
return nil
}
func isPublicTarget(addr netip.Addr) bool {
return addr.IsValid() && !addr.IsLoopback() &&
!addr.IsPrivate() && !addr.IsLinkLocalUnicast() &&
!addr.IsUnspecified()
}
这一步仍然不是网络层的最终保证:DNS解析结果可能动态变化,建立连接的时候也可能触发重新解析。生产环境还应该在出口防火墙、容器网络或代理层提前阻断内网网段;应用层校验负责给出清晰的业务拒绝提示和审计日志。
重定向不要让第一跳的校验失效
Go 标准库的HTTP客户端默认会自动处理重定向。如果第一跳是公网地址,响应却把客户端跳转到内网地址,最初的校验就完全失去了意义。最简单的方案是直接关闭自动跳转,让业务只接受一个明确的目标:
client := &http.Client{
Timeout: 8 * time.Second,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
若产品必须支持跟随跳转,不要只把 FollowRedirect 改成 true。应该限制最多跳转次数,并在 CheckRedirect 中对新请求的URL重新跑一遍scheme、端口、DNS和IP的全量校验策略。跨域跳转还要重新确认请求头,尤其不要把原目标的内部凭证直接传给新主机。
把请求封装成“校验后才连接”的统一边界
安全检查最好放在唯一的出站请求入口处,而不是散落在三个不同的handler里面。入口要限制响应体的最大大小、记录拒绝原因,并且避免把上游的原始响应完整回传给调用方。下面只展示边界的核心逻辑,具体的连接绑定和出口代理策略应由部署环境补齐。
func FetchImage(ctx context.Context, raw string) ([]byte, error) {
u, err := checkURL(raw, publicImagePolicy)
if err != nil {
return nil, err
}
if err := rejectNonPublic(ctx, u.Hostname()); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
resp, err := publicImageClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode = 300 {
return nil, fmt.Errorf("upstream status: %s", resp.Status)
}
return io.ReadAll(io.LimitReader(resp.Body, 5
如果业务对域名有固定名单,优先采用域名或来源白名单,并且把最终解析到的DNS地址也一并记录下来。支持任意公网地址的场景复杂度高很多,不能把“不是私网地址”误当成“就是完全可信目标”。
四组测试决定这条防线有没有用
安全代码最怕只测正常URL。至少把以下输入写成单元测试用例:合法HTTPS公网主机;回环和私网IP;解析到受限地址的域名;返回302的公网地址。再补上无scheme、带用户信息、非允许端口和IPv6地址这些边界场景。
func TestFetchPolicy(t *testing.T) {
cases := []struct {
name string
raw string
want bool
}{
{"public https", "https://cdn.example.test/a.png", true},
{"loopback", "https://127.0.0.1/a.png", false},
{"private", "https://10.0.0.8/a.png", false},
{"wrong port", "https://cdn.example.test:8443/a.png", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := checkURL(tc.raw, publicImagePolicy)
if (err == nil) != tc.want {
t.Fatalf("allowed=%v, err=%v", err == nil, err)
}
})
}
}
测试通过也不等于网络边界的防护已经做完。上线验收时再确认出口ACL、代理规则、超时配置、响应大小限制和日志脱敏规则;如果服务部署在云上,还要把云厂商元数据地址当作明确的拒绝对象,在网络层再加一层独立控制。

相关问题
只禁止 127.0.0.1 是否够用?
不够。还要考虑IPv6回环、私网和链路本地地址、解析到受限地址的域名,以及重定向后的新目标。
为什么不建议只用正则判断 URL?
正则很难正确覆盖scheme、用户信息、IPv6、端口和编码的各种差异。先用标准库解析,再对结构化字段做白名单判断,更容易测试和审计。
关闭重定向后,是否就完全安全了?
不是。它只能消除一条跳转路径,仍需要协议、端口、DNS/IP、响应体大小和网络出口策略共同生效。
最后的验收清单
把远程抓取接口交付前,逐项确认:输入只接受业务允许的scheme和端口;URL解析后拒绝用户信息和空主机;A/AAAA解析结果逐个检查;自动重定向默认关闭;必须跳转时每一跳重新校验;响应体有大小限制;错误日志不泄露敏感响应;出口网络还有独立的拒绝规则。
这样设计的重点不是让某个函数“看起来安全”,而是让一次出站连接在代码、测试和网络环境里都能说清楚:它为什么被允许,以及下一跳为什么仍然不能越界。
-
215 收藏
-
399 收藏
-
458 收藏
-
151 收藏
-
169 收藏
-
226 收藏
-
Golang · Go问答 | 4小时前 | 错误处理 · go · 性能 · bytes.Buffer · Go 1.26 · io.EOF 版本迁移 Go 1.26 bytes.Buffer.Peek 缓冲区预览428 收藏
-
488 收藏
-
160 收藏
-
158 收藏
-
Golang · Go问答 | 22小时前 | golang · 连接池 · database/sql · Go问答 · 数据库事务 · 连接池 事务 DBStats rows.Close Go database/sql374 收藏
-
271 收藏
-
Golang · Go问答 | 1天前 | golang · 错误处理 · 泛型 · Go问答 · Go 1.26 · errors.As Go问答 Go 1.26 errors.AsType 泛型错误处理255 收藏
-
187 收藏
-
382 收藏
-
158 收藏
-
279 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习