当前位置:首页 > 文章列表 > Golang > Go问答 > Go 回调地址怎么做 SSRF 防护:域名白名单、解析结果与跳转控制

Go 回调地址怎么做 SSRF 防护:域名白名单、解析结果与跳转控制

来源:17golang原创 2026-08-10 12:48:59 0浏览 收藏

图片抓取接口上线后,最容易被忽略的参数往往就是 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解析列为需要同时覆盖的边界校验点。

Go HTTP 客户端从用户 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、代理规则、超时配置、响应大小限制和日志脱敏规则;如果服务部署在云上,还要把云厂商元数据地址当作明确的拒绝对象,在网络层再加一层独立控制。

Go SSRF 防护从 URL 解析到 IP 校验和跳转拒绝的验证链插画

相关问题

只禁止 127.0.0.1 是否够用?

不够。还要考虑IPv6回环、私网和链路本地地址、解析到受限地址的域名,以及重定向后的新目标。

为什么不建议只用正则判断 URL?

正则很难正确覆盖scheme、用户信息、IPv6、端口和编码的各种差异。先用标准库解析,再对结构化字段做白名单判断,更容易测试和审计。

关闭重定向后,是否就完全安全了?

不是。它只能消除一条跳转路径,仍需要协议、端口、DNS/IP、响应体大小和网络出口策略共同生效。

最后的验收清单

把远程抓取接口交付前,逐项确认:输入只接受业务允许的scheme和端口;URL解析后拒绝用户信息和空主机;A/AAAA解析结果逐个检查;自动重定向默认关闭;必须跳转时每一跳重新校验;响应体有大小限制;错误日志不泄露敏感响应;出口网络还有独立的拒绝规则。

这样设计的重点不是让某个函数“看起来安全”,而是让一次出站连接在代码、测试和网络环境里都能说清楚:它为什么被允许,以及下一跳为什么仍然不能越界。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
View Transition API 跨文档过渡怎么落地:同源导航、@view-transition 与降级检查View Transition API 跨文档过渡怎么落地:同源导航、@view-transition 与降级检查
上一篇
View Transition API 跨文档过渡怎么落地:同源导航、@view-transition 与降级检查
Go 接 Ollama API 做模型健康检查:版本、模型存在性与超时处理
下一篇
Go 接 Ollama API 做模型健康检查:版本、模型存在性与超时处理
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    4787次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4378次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4323次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4563次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4507次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码