Go HTTP 出站请求如何防 SSRF:URL 解析、内网地址拦截与重定向复核
有些 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 中的用户信息,避免把凭据悄悄带进出站请求;去掉末尾根域名点只是规范化操作,不能把它当成安全判断。域名仍要在真正拨号前解析。

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

日志、响应体和超时也是安全控制
地址被拦截后只返回一个 500,排查时很难区分 DNS 失败、私网命中还是跳转次数过多。建议记录请求编号、原始主机名、解析结果、跳转次数、拒绝类别和耗时;不要记录带查询参数的完整 URL,里面可能有令牌。
响应体也要限长。一个看似安全的公网地址仍可能返回很大的内容,或者让连接长时间占用 worker。可以用 io.LimitReader 包住读取逻辑,并同时设置请求总超时、连接超时和响应头等待上限。安全边界和资源预算应该在同一份验收清单中出现。
上线前的验证清单
- 输入校验:只允许
http/https,拒绝用户信息、空主机和异常端口。 - 地址校验:覆盖回环、私网、链路本地、未指定、企业自定义网段及 IPv4 映射 IPv6。
- 拨号校验:
DialContext前重新解析,确认 Transport、代理和 DNS 行为可解释。 - 跳转校验:限制次数,每一跳重新检查,跨主机时清理敏感请求头。
- 资源校验:总超时、响应头超时、响应体上限和日志脱敏都有测试。
测试不只写一个 127.0.0.1。至少把 10.0.0.8、192.168.1.10、169.254.169.254、::1 和 ::ffff:127.0.0.1 放进表格;再用一个测试服务器模拟公网入口跳转到拒绝目标,确认请求在第二跳停止。
相关问题
只禁用 localhost 是否足够?
不够。攻击者可以直接使用 IP、其他回环写法、私网地址、链路本地地址或解析到内网的域名。应以解析后的地址和拨号出口为判断对象。
为什么不能只在 handler 里检查一次?
DNS 结果和重定向目标可能变化,真正连接由 Transport 完成。业务层检查负责快速拒绝,DialContext 和 CheckRedirect 负责守住后续边界。
禁止所有重定向是不是最简单?
如果业务不需要跳转,直接禁止最省心;如果必须跟随,就限制次数、逐跳复核地址,并在跨主机时清理敏感头。
IsPrivate 能代替全部网段规则吗?
不能。它不能表达你的企业自定义网段、云元数据策略和代理出口约束。把标准判断与业务拒绝网段合并成一份可测试策略更稳妥。
把出站请求当成一个受控能力
SSRF 防护不是给 URL 加几条黑名单,而是把“解析、解析结果、拨号、重定向、响应读取和审计”串成一条受控链路。入口检查让错误尽早失败,Transport 复核避免 DNS 变化绕过,重定向检查守住后续主机,超时和响应体上限则限制攻击成本。
当这几个点都有单测和日志证据后,新增一个远程抓取功能才有清晰的评审边界:允许访问哪些网段、是否允许跳转、代理从哪里来、失败如何告警,都能在代码和验收表里找到答案。
Go WebAssembly 怎么读写 localStorage:syscall/js 的边界、异常与加载检查
- 上一篇
- Go WebAssembly 怎么读写 localStorage:syscall/js 的边界、异常与加载检查
- 下一篇
- Go 泛型 comparable 约束怎么选:map key、接口值与编译期边界
-
- Golang · Go教程 | 1小时前 | [] · []
- Go 依赖被替换怎么查:GOSUMDB、GOPRIVATE 与私有模块边界
- 413浏览 收藏
-
- Golang · Go教程 | 1小时前 | [] · []
- Go 依赖被替换怎么查:GOSUMDB、GOPRIVATE 与私有模块边界
- 351浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go 1.24 os.Root 如何限制文件系统越界:路径校验、符号链接与兼容边界
- 437浏览 收藏
-
- Golang · Go教程 | 3小时前 |
- Go 泛型 comparable 约束怎么选:map key、接口值与编译期边界
- 486浏览 收藏
-
- Golang · Go教程 | 1天前 |
- Go slices.SortFunc 怎么选比较器:等值排序、稳定性与三种排序边界
- 397浏览 收藏
-
- Golang · Go教程 | 1天前 |
- Go html/template 用户可控链接怎么防止协议注入:上下文转义、URL 白名单与回归测试
- 292浏览 收藏
-
- Golang · Go教程 | 1天前 | 错误处理 · 事务 · go · database/sql · Go database/sql Rows.Err
- Go database/sql 查完数据为什么还要检查 Rows.Err:连接中断、Close 与事务边界
- 102浏览 收藏
-
- Golang · Go教程 | 1天前 | 容器 · go · 性能 · kubernetes · 运行时 · Kubernetes GOMAXPROCS cgroup Go 1.25 容器 CPU 限额
- Go 1.25 容器里的 GOMAXPROCS 怎么迁移:cgroup CPU 限额、自动更新与旧环境兼容
- 438浏览 收藏
-
- Golang · Go教程 | 1天前 | 标准库 · go · csv · 数据导入 · 错误定位 · CSV导入 encoding/csv Go教程 FieldsPerRecord LazyQuotes
- Go encoding/csv 处理不齐列文件:FieldsPerRecord、LazyQuotes 与逐行错误定位
- 216浏览 收藏
-
- Golang · Go教程 | 1天前 | 字符串 · go · 命令解析 · 命令行参数 Go strings.Fields 字符串分词 引号转义
- Go strings.Fields 不能正确解析带引号命令参数:分词边界、转义与测试
- 172浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 4787次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4377次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4323次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4563次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4507次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Golang实现HTTP编程请求和响应
- 2022-12-28 101浏览
-
- golangNewRequest/gorequest实现http请求的示例代码
- 2023-01-24 343浏览
-
- 一文详解Golang中net/http包的实现原理
- 2022-12-29 419浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览

