背景
SSRF 这类问题在很多业务里并不显眼:一个图片抓取、Webhook 回调、URL 预览、文档导入功能,都可能引入服务端发起请求的能力。真正麻烦的是,它往往不是单点 bug,而是 URL 解析、网络边界、重定向处理、云环境元数据接口、日志可观测性共同作用后的结果。
这篇文章整理的是我在代码审计和风险复盘中比较常见的一套处理思路,重点放在如何识别风险、如何验证边界、以及如何把修复做稳定。内容不涉及未授权攻击操作,适合用于自查、审计和安全加固。
典型场景
业务侧常见的需求是:用户提交一个 URL,服务端去拉取内容并进行后续处理,例如生成缩略图、解析 Open Graph 信息、拉取远程配置、回调第三方地址等。
一个简化后的逻辑通常类似这样:
String url = request.getParameter('url');
byte[] body = httpClient.get(url);
return parse(body);如果这里没有做严格约束,服务端就变成了一个“代请求者”。风险不一定只在访问内网地址,也可能出现在以下几个位置:
- 通过重定向访问到非预期地址。
- URL 解析差异导致校验与实际请求目标不一致。
- DNS 解析结果变化,出现 DNS rebinding 风险。
- 访问云厂商元数据地址或本机管理端口。
- 协议未限制,导致 file、gopher、ftp 等非预期协议被客户端支持。
- 响应内容未限制,造成资源消耗或信息泄露。
问题分析
SSRF 的核心不是“能不能请求一个 URL”,而是“服务端是否能够被用户控制去访问不该访问的资源”。审计时我一般会拆成四个问题看:
- 输入是否完全由用户控制。
- 请求发起点是否在服务端网络环境中。
- 是否存在内网、环回、链路本地地址、云元数据地址等敏感目标。
- 是否允许跳转、代理、非 HTTP 协议或大响应体。
如果这四项同时满足,就需要按高风险处理。即使当前只能访问公网,也建议按“潜在 SSRF 面”纳入治理,因为后续业务网络变更、容器迁移、云环境接入后,风险会被放大。
代码审计关注点
审计时不要只搜索 httpClient.get 这一类直观调用,也要关注封装后的工具类和第三方 SDK。常见关键词包括:
URL
URI
HttpClient
OkHttpClient
RestTemplate
WebClient
URLConnection
fetch
request
axios
curl_exec
file_get_contents重点看三类位置:
- 用户可控参数进入请求方法前是否经过白名单校验。
- 是否手动处理 30x 跳转,或者客户端默认跟随跳转。
- 是否在请求前后都校验了解析后的 IP 地址。
这里有一个经常被忽略的点:只校验字符串是不够的。例如判断 URL 中是否包含某个域名,或者简单判断是否以 开头,都容易被 URL 解析差异绕开。更稳妥的做法是先使用标准库解析,再基于 scheme、host、port、解析后的 IP 做判断。
推荐的修复策略
治理 SSRF,优先推荐“允许列表”而不是“禁止列表”。禁止列表很难覆盖完整,尤其是 IPv6、十进制/八进制 IP、域名解析变化、特殊地址段等情况。
1. 限制协议和端口
大多数业务只需要 HTTP/HTTPS,并且端口也可以收敛到 80、443 或业务明确需要的端口。
allowedSchemes = ['http', 'https']
allowedPorts = [80, 443]如果业务没有强需求,不建议允许 file、ftp、gopher 等协议。部分语言或客户端库会隐式支持多协议,使用前要确认默认行为。
2. 使用域名允许列表
如果业务是访问固定第三方,建议配置允许列表,例如只允许访问指定供应商域名。注意要做规范化处理,避免大小写、尾点、Punycode 等问题导致判断失真。
allowedHosts:
- api.partner.example
- static.partner.example允许列表不要写成后缀模糊匹配,尤其不要简单使用 endsWith。比如 badexample.com 和 example.com 在字符串上很容易被错误处理。确实需要支持子域时,也要明确边界,例如 host == example.com 或 host 以 .example.com 结尾。
3. 解析 IP 后再判断地址段
域名最终会解析为 IP,真正请求的是 IP 对应的网络位置。建议在请求前解析 DNS,并拒绝以下地址段:
- 127.0.0.0/8 环回地址。
- 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 私有地址。
- 169.254.0.0/16 链路本地地址。
- 0.0.0.0/8、224.0.0.0/4 等特殊地址。
- ::1、fc00::/7、fe80::/10 等 IPv6 特殊地址。
云环境还要特别关注元数据服务地址,例如链路本地地址上的实例元数据服务。即使应用层做了限制,也建议在网络层做出口策略兜底。
4. 禁止或严格处理重定向
很多 SSRF 问题出在第一次请求是合法地址,但服务端返回 302 跳转到敏感地址。修复时可以直接禁止自动重定向;如果业务必须支持重定向,需要对每一次跳转后的目标重新执行完整校验。
// 伪代码
response = httpClient.request(url, followRedirects=false)
if response.status in [301, 302, 303, 307, 308]:
nextUrl = response.header('Location')
validate(nextUrl)
request(nextUrl)5. 控制请求资源
SSRF 不只导致访问敏感地址,也可能被用于打满连接池、拖慢后端或触发大文件下载。建议设置:
- 连接超时和读取超时。
- 最大响应体大小。
- 最大重定向次数。
- 并发与速率限制。
- 只读取必要的响应头或前 N 字节。
connectTimeout: 3s
readTimeout: 5s
maxResponseSize: 2MB
maxRedirects: 0网络层兜底
应用层修复很重要,但不要把所有责任都放在业务代码里。更稳定的做法是应用层校验加网络层出口控制。
在容器或云环境中,可以考虑:
- 业务容器默认禁止访问内网敏感网段。
- 只允许访问必要的第三方域名或代理出口。
- 元数据服务使用云厂商推荐的增强保护机制。
- 将外部抓取类请求统一走受控代理,由代理做地址校验、日志和限流。
如果使用 Nginx 或网关作为出口代理,可以在代理层做基础限制。以下示例仅表达思路,实际规则需要结合网段和业务调整:
resolver 8.8.8.8 valid=30s;
proxy_connect_timeout 3s;
proxy_read_timeout 5s;
client_max_body_size 2m;更严格的场景建议在防火墙、安全组、Kubernetes NetworkPolicy 或 eBPF 层做出站访问控制,避免应用代码遗漏造成绕过。
验证与测试方法
验证 SSRF 修复时,不建议在生产环境随意探测内网地址。比较稳妥的方式是搭建受控测试环境,准备几个明确的测试目标:
- 允许访问的正常公网域名。
- 不允许访问的私有地址测试服务。
- 返回 302 的测试服务,跳转到不允许访问的地址。
- 大响应体和慢响应测试服务。
- IPv6 与特殊 IP 表达形式测试用例。
可以用本地简单服务模拟重定向行为,确认应用是否会对跳转后的地址重新校验:
python3 -m http.server 8000对于单元测试,建议把 URL 校验函数独立出来,覆盖以下用例:
- http 与 https 正常用例。
- 非预期协议。
- localhost、127.0.0.1、::1。
- 私有网段地址。
- 域名尾点与大小写。
- 重定向后的目标地址。
- 解析失败或多 IP 返回。
日志与告警
SSRF 风险治理完成后,日志也要补上。建议记录但不要记录敏感响应体:
- 原始 URL。
- 规范化后的 host、scheme、port。
- DNS 解析结果。
- 是否命中拦截规则。
- 请求耗时、响应大小、状态码。
告警规则可以关注短时间内大量不同目标、访问特殊地址段、频繁触发重定向拦截、响应体异常过大等行为。这类日志对后续排查很有价值,也能帮助发现业务误配置。
常见误区
- 只检查字符串中是否包含内网 IP:无法覆盖域名解析、IPv6 和特殊表示法。
- 只在第一次请求前校验:重定向后的地址同样需要校验。
- 只做应用层限制:一旦某个调用点遗漏,风险仍然存在。
- 把黑名单写死:网段、云环境和业务拓扑变化后容易失效。
- 忽略响应大小和超时:即使无法访问敏感地址,也可能造成资源消耗。
总结
SSRF 的治理思路可以概括为三句话:入口收敛、目标校验、出口兜底。代码层面要基于标准解析和允许列表做校验,网络层面要限制服务端的出站能力,运行阶段要有日志和告警支撑。
实际落地时,不要只修一个接口。建议把“服务端根据用户输入发起请求”的能力统一封装,形成公共组件:统一协议限制、统一 DNS 与 IP 校验、统一重定向策略、统一超时和响应大小控制。这样后续业务新增类似功能时,安全边界会稳定很多。
推荐意见