当前位置:首页 > 文章列表 > Golang > Go教程 > Go HTTP 出站请求如何防 SSRF:URL 解析、内网地址拦截与重定向复核

Go HTTP 出站请求如何防 SSRF:URL 解析、内网地址拦截与重定向复核

来源:17golang原创 2026-08-10 11:49:58 0浏览 收藏

有些 Go 服务会接收一个 URL,再替用户抓取网页、校验图片或读取远程配置。功能上线后,攻击者把参数换成 http://127.0.0.1:8080 只是最直观的测试;更麻烦的是域名解析、IPv4 映射 IPv6、十进制地址和 30x 跳转都能改变真正的出站目标。

可行的防护边界是:先解析并规范化 URL,只允许 http/https,解析出地址后拒绝回环、私网、链路本地和未指定地址;连接时仍使用同一套判断逻辑,重定向到新主机时再次校验。URL 字符串里没出现“localhost”,不代表请求就是安全的。

要点速览
  • 安全判断的对象是解析后的 IP 和最终连接目标,不是 URL 字符串。
  • netip.Addr 要同时处理回环、私网、链路本地、未指定和 IPv4 映射 IPv6。
  • http.Client.CheckRedirect 只能控制跳转策略,不能替代每次目标地址复核。
  • 代理、DNS、超时和响应体上限属于出站资源边界,要和地址策略一起验收。

先把真正要保护的资产列出来

SSRF 的危害不只是一条内网 URL 被访问。Go 进程所在网络里的管理端口、云环境元数据地址、调试接口和带有内网凭据的服务,往往才是高价值目标。应用本身也可能因为读取了响应体,把内部错误信息或配置内容继续返回给外部调用方。

攻击路径常见目标需要留下的证据
回环/私网直连127.0.0.1、10/8、172.16/12、192.168/16规范化地址、拒绝原因、请求编号
域名解析变化公开域名解析到内网地址主机名、解析结果、连接目标
重定向绕过公网入口跳到内网服务每一跳 URL、状态码、最终判定
代理绕过HTTP_PROXY 或自定义 Transport代理策略、DialContext、超时

字符串校验为什么挡不住真实目标

strings.Contains(host, "localhost") 只能拦住一个拼写。主机名可能解析到私网地址,地址还可能用 IPv6 表示;而 url.Parse 解析出的 Host 也可能包含端口,不能直接拿整个字符串做网段判断。

第一步应该把协议、主机名和端口拆开,并拒绝没有主机名的 URL。下面的函数只负责解析,不负责联网,适合作为输入门禁。

package outbound

import (
    "fmt"
    "net"
    "net/url"
    "strings"
)

func parseTarget(raw string) (*url.URL, string, error) {
    u, err := url.ParseRequestURI(raw)
    if err != nil || u.User != nil {
        return nil, "", fmt.Errorf("invalid target")
    }
    if u.Scheme != "http" && u.Scheme != "https" {
        return nil, "", fmt.Errorf("scheme is not allowed")
    }
    host := strings.TrimSuffix(u.Hostname(), ".")
    if host == "" || net.ParseIP(host) != nil {
        return u, host, nil
    }
    return u, strings.ToLower(host), nil
}

这里拒绝了 URL 中的用户信息,避免把凭据悄悄带进出站请求;去掉末尾根域名点只是规范化操作,不能把它当成安全判断。域名仍要在真正拨号前解析。

Go net/url 解析请求后经过 netip 地址判断,公网目标放行、内网目标拦截的前后指标对比图

用 netip 判断地址,并覆盖 IPv4 与 IPv6 边界

仅判断 IsPrivate 还不够。回环地址、链路本地地址和未指定地址需要明确拒绝;IPv4 映射 IPv6 也必须先还原成 IPv4 语义,否则 ::ffff:127.0.0.1 可能漏过只看 IPv4 的代码。

package outbound

import "net/netip"

func blockedAddr(addr netip.Addr) bool {
    if addr.Is4In6() {
        addr = addr.Unmap()
    }
    return !addr.IsValid() ||
        addr.IsLoopback() ||
        addr.IsPrivate() ||
        addr.IsLinkLocalUnicast() ||
        addr.IsUnspecified()
}

生产策略通常还会维护企业自己的拒绝网段,例如容器网桥、服务网格地址和云厂商元数据网段。它们不要散落在多个 handler 里,应该集中成可审计的策略函数,并为每个拒绝原因写表格驱动测试。

地址判断通过后,不能直接把解析结果丢给一个默认客户端就结束了。DNS 可能在解析和连接之间变化,所以拨号函数也要重新解析并检查候选地址。

把检查放到 DialContext,守住真正的连接出口

安全策略最怕只存在于业务层。业务层可以检查原始 URL,但真正建立 TCP 连接的是 Transport。把地址判断放进自定义 DialContext,至少能让每次拨号前再次确认 IP 不在拒绝范围。

package outbound

import (
    "context"
    "fmt"
    "net"
    "net/netip"
    "time"
)

func dialAllowed(ctx context.Context, network, address string) (net.Conn, error) {
    host, port, err := net.SplitHostPort(address)
    if err != nil {
        return nil, fmt.Errorf("bad address: %w", err)
    }
    ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
    if err != nil {
        return nil, fmt.Errorf("resolve target: %w", err)
    }
    for _, ip := range ips {
        if blockedAddr(ip) {
            continue
        }
        d := net.Dialer{Timeout: 3 * time.Second}
        return d.DialContext(ctx, network, net.JoinHostPort(ip.String(), port))
    }
    return nil, fmt.Errorf("all resolved addresses are blocked")
}

示例把第一个可用地址作为最小实现,真实项目要根据 IPv4/IPv6 双栈策略处理失败后切换,并记录 DNS 结果。更重要的是,不要让 Transport 又通过一个未经审计的代理改变出口;代理策略应显式配置,不能把环境变量当成安全边界。

重定向每一跳都重新检查

公网 URL 返回 302,并不意味着后续仍在公网。CheckRedirect 收到新请求时,至少要重新检查协议、主机名和解析后的地址;跨主机跳转时还应清理原请求中的 Authorization 等敏感头。

client := &http.Client{
    Timeout: 5 * time.Second,
    CheckRedirect: func(next *http.Request, history []*http.Request) error {
        if len(history) >= 3 {
            return http.ErrUseLastResponse
        }
        if next.URL.Scheme != "http" && next.URL.Scheme != "https" {
            return fmt.Errorf("redirect scheme is not allowed")
        }
        if err := verifyResolvedHost(next.Context(), next.URL.Hostname()); err != nil {
            return err
        }
        next.Header.Del("Authorization")
        return nil
    },
}

resp, err := client.Get(target)
if err != nil {
    return err
}
defer resp.Body.Close()

这段代码里的 verifyResolvedHost 应复用同一地址策略,而不是再写一份“看起来差不多”的判断。重定向次数限制解决的是资源消耗,地址复核解决的是边界绕过,两者不能相互替代。

Go http.Client 重定向从公网入口跳向内网地址时,在每一跳重新解析并拦截的前后指标对比图

日志、响应体和超时也是安全控制

地址被拦截后只返回一个 500,排查时很难区分 DNS 失败、私网命中还是跳转次数过多。建议记录请求编号、原始主机名、解析结果、跳转次数、拒绝类别和耗时;不要记录带查询参数的完整 URL,里面可能有令牌。

响应体也要限长。一个看似安全的公网地址仍可能返回很大的内容,或者让连接长时间占用 worker。可以用 io.LimitReader 包住读取逻辑,并同时设置请求总超时、连接超时和响应头等待上限。安全边界和资源预算应该在同一份验收清单中出现。

上线前的验证清单

  • 输入校验:只允许 http/https,拒绝用户信息、空主机和异常端口。
  • 地址校验:覆盖回环、私网、链路本地、未指定、企业自定义网段及 IPv4 映射 IPv6。
  • 拨号校验:DialContext 前重新解析,确认 Transport、代理和 DNS 行为可解释。
  • 跳转校验:限制次数,每一跳重新检查,跨主机时清理敏感请求头。
  • 资源校验:总超时、响应头超时、响应体上限和日志脱敏都有测试。

测试不只写一个 127.0.0.1。至少把 10.0.0.8192.168.1.10169.254.169.254::1::ffff:127.0.0.1 放进表格;再用一个测试服务器模拟公网入口跳转到拒绝目标,确认请求在第二跳停止。

相关问题

只禁用 localhost 是否足够?

不够。攻击者可以直接使用 IP、其他回环写法、私网地址、链路本地地址或解析到内网的域名。应以解析后的地址和拨号出口为判断对象。

为什么不能只在 handler 里检查一次?

DNS 结果和重定向目标可能变化,真正连接由 Transport 完成。业务层检查负责快速拒绝,DialContext 和 CheckRedirect 负责守住后续边界。

禁止所有重定向是不是最简单?

如果业务不需要跳转,直接禁止最省心;如果必须跟随,就限制次数、逐跳复核地址,并在跨主机时清理敏感头。

IsPrivate 能代替全部网段规则吗?

不能。它不能表达你的企业自定义网段、云元数据策略和代理出口约束。把标准判断与业务拒绝网段合并成一份可测试策略更稳妥。

把出站请求当成一个受控能力

SSRF 防护不是给 URL 加几条黑名单,而是把“解析、解析结果、拨号、重定向、响应读取和审计”串成一条受控链路。入口检查让错误尽早失败,Transport 复核避免 DNS 变化绕过,重定向检查守住后续主机,超时和响应体上限则限制攻击成本。

当这几个点都有单测和日志证据后,新增一个远程抓取功能才有清晰的评审边界:允许访问哪些网段、是否允许跳转、代理从哪里来、失败如何告警,都能在代码和验收表里找到答案。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go WebAssembly 怎么读写 localStorage:syscall/js 的边界、异常与加载检查Go WebAssembly 怎么读写 localStorage:syscall/js 的边界、异常与加载检查
上一篇
Go WebAssembly 怎么读写 localStorage:syscall/js 的边界、异常与加载检查
Go 泛型 comparable 约束怎么选:map key、接口值与编译期边界
下一篇
Go 泛型 comparable 约束怎么选:map key、接口值与编译期边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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为你免费谱曲,轻松做同款!
    4377次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4323次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4563次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4507次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码