背景
SSRF(Server-Side Request Forgery)这类问题在代码审计和漏洞复现里很常见,但真正处理起来并不只是“禁用内网地址”这么简单。很多业务会天然需要服务端发起请求,例如:远程图片抓取、Webhook 回调、在线文档预览、URL 健康检查、第三方接口转发等。如果只做简单黑名单,通常很容易被各种地址表示法、跳转链、DNS 解析变化绕过去。
这篇文章整理一个偏工程化的 SSRF 分析方法,重点放在安全验证、漏洞定位和加固落地。内容不会展开攻击性利用,只讨论如何识别风险、复现边界和修复设计。
典型场景
下面这些接口在审计中需要重点关注:
- 根据用户提交的 URL 抓取图片、头像、封面、文件。
- Webhook、Callback、OpenAPI 回调地址配置。
- 在线代理、站点截图、URL 预览、网页转 PDF。
- 后端请求第三方资源,并将响应内容返回给前端。
- 文件导入功能支持 HTTP/HTTPS 远程地址。
一个常见的风险接口可能长这样:
POST /api/fetch-image HTTP/1.1
Content-Type: application/json
{
"url": "
}如果服务端直接拿这个 url 发请求,并把结果返回或保存,就需要考虑 SSRF 风险。
问题本质
SSRF 的核心不是“请求了一个 URL”,而是“用户可控输入影响了服务端的网络访问目标”。服务端一般处在更高权限的网络位置,可能能访问到公网用户无法访问的内部服务、云厂商元数据接口、管理面板、Redis、Elasticsearch、Kubernetes API 等。
从代码角度看,危险模式通常是:
// 伪代码,仅用于说明风险模式
url := request.json["url"]
resp := httpClient.Get(url)
return resp.Body这段代码的问题不在于使用了 HTTP 客户端,而在于没有对访问目标、协议、解析结果、跳转行为和响应体做任何约束。
技术分析
1. URL 解析不一致
不同语言、不同库对 URL 的解析行为不完全一致。业务代码校验时使用一个解析器,真正请求时 HTTP 客户端又使用另一个解析器,就可能产生判断差异。审计时要确认校验和请求是否基于同一套解析结果。
建议至少检查以下字段:
- scheme:只允许 http、https。
- host:必须存在,且不能是空值。
- port:只允许业务必要端口,例如 80、443。
- userinfo:不建议允许,例如 URL 中的特殊字符、编码、大小写、尾点域名、IPv6 表示等情况。修复时不要依赖字符串 contains、startsWith 这类简单判断。
2. DNS 解析与地址校验
只校验域名字符串是不够的。安全判断应该发生在 DNS 解析之后,对解析得到的 IP 地址进行分类检查。需要拒绝的地址范围一般包括:
- loopback:127.0.0.0/8、::1。
- private:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。
- link-local:169.254.0.0/16、fe80::/10。
- multicast、reserved、unspecified。
- 云元数据服务常见地址,例如 169.254.169.254。
另外要注意 DNS Rebinding。也就是第一次解析通过校验,实际请求时又解析到内部地址。稳妥做法是:由应用自己解析域名并校验 IP,然后建立连接时绑定到已校验的 IP,同时保留原始 Host/SNI 的业务需求;或者在网络层做 egress 策略,避免应用层校验成为唯一防线。
3. 跳转链风险
很多 HTTP 客户端默认跟随 301、302、307、308 跳转。如果只检查初始 URL,不检查跳转后的 Location,仍然存在风险。建议禁用自动跳转,手动处理每一次 Location,并对新的 URL 重复完整校验。
// Go 示例:禁用自动跳转,避免只校验首个 URL client := &http.Client{ Timeout: 5 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, }如果业务确实需要跳转,建议限制最大跳转次数,例如不超过 3 次,并要求每次跳转后的 scheme、host、解析 IP 都通过校验。
4. 协议与客户端能力
SSRF 不一定只发生在 HTTP。部分库或组件支持 file、gopher、ftp、dict、jar 等协议,或者在某些封装层里会自动处理特殊协议。实际审计时要看最终调用的客户端支持什么,不要只看接口名。
一般建议:
- 明确只允许 http 和 https。
- 拒绝非标准端口,除非业务确实需要。
- 不要把用户输入直接传给支持多协议的通用下载器。
- 对响应类型和大小做限制,避免被当成内网探测或大文件下载通道。
关键步骤:一次安全复现与确认流程
为了避免测试行为影响线上环境,建议在本地或测试环境搭建最小复现。流程如下:
1. 定位数据流
先确认用户输入从入口到服务端请求的位置。代码审计时重点搜索:
curl_exec file_get_contents fsockopen requests.get urllib.request http.Get RestTemplate WebClient OkHttpClient HttpClient同时关注图片处理、导入导出、回调配置等业务代码,不要只搜“url”。很多变量名可能是 avatar、callback、endpoint、source、remotePath。
2. 在测试环境验证外连能力
可以准备一个自己控制的测试 HTTP 服务,用于确认服务端是否会访问用户提交的地址。这里只做外连确认,不进行内网探测。
python3 -m http.server 8080提交测试 URL 后观察测试服务日志。如果能看到服务端请求,说明入口具备服务端请求能力,后续需要检查限制是否充分。
3. 验证过滤边界
在授权测试环境里,可以围绕以下点做边界验证:
- 是否限制协议,只允许 http/https。
- 是否允许跳转,跳转后的地址是否重新校验。
- 是否校验 DNS 解析后的 IP。
- IPv4、IPv6、域名尾点、大小写、编码后 URL 是否处理一致。
- 是否限制响应大小、超时时间、Content-Type。
这里的重点是验证防护逻辑是否完整,而不是扩大影响面。生产环境不建议做内网地址访问测试。
加固方案
1. 优先使用白名单
如果业务场景固定,例如只允许访问企业自己的对象存储域名、固定第三方 API 域名,白名单是最可靠的方式。不要用“禁止 127.0.0.1、localhost”这类黑名单作为主要防线。
// 伪代码:基于业务白名单校验 host allowedHosts := map[string]bool{ "img.example-cdn.com": true, "api.partner.example": true, } if !allowedHosts[parsed.Hostname()] { return error("host not allowed") }如果必须允许用户输入任意公网 URL,则需要组合使用 URL 规范化、DNS 解析校验、跳转校验、网络出口限制。
2. 解析后校验 IP 范围
校验逻辑应基于解析后的 IP,而不是原始字符串。下面是一个偏思路化的 Go 示例:
func isBlockedIP(ip net.IP) bool { if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsMulticast() || ip.IsUnspecified() { return true } // 额外阻断云元数据地址 if ip.String() == "169.254.169.254" { return true } return false }实际项目里还要补充 IPv6、保留地址段和公司内部网段。不要只判断字符串前缀。
3. 控制 HTTP 客户端行为
- 设置较短超时,例如连接超时 2 秒、整体超时 5 秒。
- 禁用自动跳转,或对每次跳转重新校验。
- 限制最大响应体大小,例如 5MB。
- 限制 Content-Type,例如只允许 image/png、image/jpeg。
- 禁止携带内部凭据、Cookie、Authorization 头。
- 不要把响应体原样返回给用户,避免变成代理通道。
// Go 示例:限制响应体大小 resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() limited := io.LimitReader(resp.Body, 5*1024*1024) body, err := io.ReadAll(limited)4. 网络层兜底
应用层校验容易因为边界情况遗漏,所以建议配合网络层策略:
- 容器或主机层限制出站访问,只允许访问必要公网地址。
- 云环境中阻断对元数据服务的非必要访问。
- 使用独立的下载服务,放在低权限网络区域。
- 对服务端主动外连建立审计日志,包括目标域名、解析 IP、状态码、耗时。
在 Kubernetes 环境,可以使用 NetworkPolicy 限制 Pod 出站访问。具体策略要结合 CNI 能力和业务依赖设计,不能只写一条“允许全部”。
代码审计检查清单
检查项 关注点 建议 URL 来源 是否由用户完全可控 优先改为白名单或后端固定配置 协议限制 是否允许 file、ftp 等非 HTTP 协议 只允许 http/https DNS 校验 是否只判断域名字符串 解析后校验 IP 分类 跳转处理 是否默认跟随 30x 禁用自动跳转或逐跳校验 响应限制 是否无限读取响应体 限制大小、类型和超时 凭据泄露 是否转发 Cookie、Token 外连请求使用干净 Header 网络出口 服务是否能访问内网敏感段 通过防火墙或 NetworkPolicy 兜底 常见误区
- 只过滤 localhost。实际还有 127.0.0.1、::1、内网段、link-local、各种域名解析情况。
- 只在前端校验 URL。服务端必须独立校验。
- 只校验第一次请求。跳转后的地址同样需要校验。
- 认为 HTTPS 就安全。HTTPS 只能保证传输安全,不代表目标地址可信。
- 把 SSRF 当成单点漏洞修补。更合理的是把外连能力当成高风险能力统一治理。
事件复盘建议
如果已经发现 SSRF 风险,复盘时建议关注三条线:
- 影响面:哪些接口存在服务端外连能力,是否可被未授权或低权限用户触发。
- 可达性:相关服务能访问哪些内部网段、云元数据、管理接口。
- 数据暴露:接口是否会返回响应体、响应头、错误信息或时间差。
日志侧建议检索异常目标地址、短时间大量不同 URL、访问失败率异常、非业务域名请求等。对于历史日志不足的系统,至少在修复后补充外连审计,方便后续追踪。
总结
SSRF 的治理重点不是写一个复杂正则,而是建立完整的访问目标控制链路:输入规范化、协议限制、白名单优先、DNS 解析后校验、跳转逐次校验、HTTP 客户端收敛、网络出口兜底。单靠某一层很难覆盖所有边界。
从长期维护角度看,建议把“服务端主动访问外部 URL”抽象成统一组件,业务侧不要各自实现下载逻辑。统一组件里沉淀校验、日志、超时、响应大小限制和安全策略,后续审计和加固都会轻松很多。
服务器、云资产与权限边界梳理示意
推荐意见