SSRF(服务端请求伪造)在实际项目中并不难被忽略:业务代码往往只是接收一个 URL,调用 HTTP 客户端抓取内容,再把结果返回给用户。问题在于,请求是由服务器发出的,攻击者可以借此让服务器访问本不应暴露的内部服务、云平台元数据接口或管理端口。
这类问题的难点通常不在于发现一个危险函数,而在于判断 URL 是否真正受控、校验是否覆盖了完整请求链路,以及 DNS 解析、重定向、代理配置和 IPv6 等边界条件是否被考虑。下面以代码审计和修复为主线,整理一套适合项目落地的分析方法。
一、常见业务场景与风险边界
以下功能都可能引入服务端请求:
- 头像、网页缩略图或远程文件导入。
- Webhook、第三方回调测试、站点连通性检测。
- PDF、Markdown、RSS、在线文档或图片转换。
- 支付、消息、监控系统中的 URL 探测功能。
- 后台管理页面提供的接口调试或数据同步功能。
审计时不能只关注参数名为 url 的接口。只要用户输入最终参与了 HTTP、HTTPS、TCP 或其他网络客户端调用,都应纳入检查范围。
判断 SSRF 风险的核心问题不是“有没有发请求”,而是“请求的目标是否由不可信输入决定,以及应用是否能证明该目标属于允许范围”。
二、代码审计的切入点
第一步是从网络请求函数反向追踪输入来源。不同语言中常见的危险调用包括 Java 的 HttpClient、RestTemplate、URL.openConnection,Python 的 requests、urllib.request,Node.js 的 axios、fetch 和底层 http.request。
建议按以下路径建立数据流:
- 入口:HTTP 参数、JSON 字段、数据库中的可编辑配置、消息队列内容。
- 处理:拼接协议、补全路径、解析 URL、跟随重定向、设置代理。
- 出口:HTTP 客户端、DNS 查询、Socket 连接、文件读取或云 SDK。
- 响应:是否将响应体、响应头、错误信息或状态码返回给用户。
如果只看到类似下面的代码,不能简单认为参数已经安全:
String target = request.getParameter("target");
return httpClient.get(target);即使外层增加了字符串包含判断,也需要继续追踪实际连接过程。URL 解析器可能会标准化主机名,HTTP 客户端可能自动跟随重定向,代理层也可能改变最终请求目标。
三、典型校验缺陷
1. 只校验字符串前缀
使用 startsWith、contains 或正则表达式匹配域名,通常无法可靠区分主机、端口、用户名信息和路径。例如,字符串看起来包含允许域名,不代表真正的主机就是该域名。正确做法是先使用标准 URL 解析器提取协议、主机、端口等结构化字段,再进行逐项判断。
2. 只禁止常见内网地址
仅拦截某几个 IPv4 网段是不够的。需要同时考虑:
- IPv4 私网、回环、链路本地和保留地址。
- IPv6 回环、唯一本地地址、链路本地地址。
- IPv4 映射 IPv6 地址。
- 十进制、八进制、十六进制或非标准形式的 IP 表示。
- 解析到内网地址的域名。
- 本地 Unix Socket、代理转发和其他非 HTTP 出口。
3. 校验一次、解析多次
这是实际项目中比较容易漏掉的边界。应用先解析域名得到公网地址并通过校验,随后 HTTP 客户端再次解析域名时,结果可能已经变化。即使没有恶意场景,DNS 轮询也可能让同一个域名对应多个地址。
因此,安全控制应尽量让“校验的地址”和“实际连接的地址”保持一致。对于高风险服务,可以采用出口代理统一解析与访问,或者在网络层直接禁止应用访问内网网段,避免仅依赖应用层判断。
4. 忽略重定向
初始 URL 可能是允许访问的公网地址,但服务器响应重定向后,客户端继续访问了不允许的目标。防护上应默认关闭自动重定向;如果业务必须支持重定向,则每一跳都重新解析、校验协议、主机、端口和 IP,并限制最大跳转次数。
四、推荐的防护策略
SSRF 防护最好采用多层控制,而不是依赖单个函数完成。比较稳妥的顺序如下:
- 明确业务是否真的需要用户指定完整 URL。能使用资源 ID、固定域名或枚举值时,不要开放任意 URL。
- 只允许业务需要的协议,通常只保留 HTTPS,拒绝文件、FTP、Gopher 等非必要协议。
- 使用标准解析器解析 URL,拒绝缺少主机、包含用户名密码或使用异常端口的地址。
- 解析所有 DNS 结果,逐个检查是否属于禁止网段。
- 关闭自动重定向,或对每次跳转重新执行完整校验。
- 限制连接超时、读取超时、响应体大小和最大下载时间。
- 通过网络出口策略限制应用服务器访问管理网、数据库网和云元数据地址。
- 记录目标主机、解析地址、状态码、耗时和拒绝原因,便于审计和事件复盘。
以下是一个偏防御性的 Java 示例,展示结构化校验思路。生产环境还需要结合 IPv6、代理和 DNS 解析策略进一步完善:
URI uri = URI.create(input);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw new SecurityException("unsupported scheme");
}
if (uri.getUserInfo() != null || uri.getHost() == null) {
throw new SecurityException("invalid authority");
}
int port = uri.getPort() == -1 ? 443 : uri.getPort();
if (port != 443) {
throw new SecurityException("unsupported port");
}
InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
for (InetAddress address : addresses) {
if (address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()) {
throw new SecurityException("private or local address");
}
}
// 生产环境应通过受控客户端发起请求,并关闭自动重定向
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(5))
.GET()
.build();需要注意,isSiteLocalAddress() 不能覆盖所有特殊地址,也不能自动解决 DNS 重绑定问题。这个示例的重点是说明校验应基于解析后的结构化信息,而不是把它当作完整的通用库。
五、测试与验证方法
测试时不建议直接对真实内网资产进行探测。可以在本地或隔离测试网络中准备几个模拟服务:
- 一个正常的 HTTPS 测试站点。
- 一个返回重定向的测试站点。
- 一个绑定本地回环地址的模拟服务。
- 一个返回超大响应或长时间不结束的服务,用于验证资源限制。
- 一个拥有多个解析地址的测试域名,用于检查多记录处理逻辑。
验证重点包括:
- 是否拒绝非 HTTPS 协议。
- 是否拒绝显式非标准端口。
- 域名解析到任意禁止地址时是否拒绝。
- 响应重定向到禁止地址时是否停止。
- 连接超时和读取超时是否生效。
- 响应体超过限制后是否及时中断。
- 错误信息是否泄露内部地址、响应头或网络拓扑。
如果项目使用代理,还要单独验证代理行为。某些客户端配置了环境变量代理或全局代理后,应用层看到的目标和实际出口可能不同。审计时应同时检查客户端默认配置、容器环境变量以及服务网格的出站策略。
六、代码审计中的几个容易忽视的点
- 异步任务:接口本身没有直接返回响应,不代表风险消失。需要继续检查任务执行器和结果存储位置。
- 图片与文件解析:缩略图、文档转换器可能在解析资源引用时再次发起网络请求,风险点不一定在主业务代码中。
- 错误信息:将底层异常原样返回可能泄露内网 IP、端口、代理地址和 DNS 信息。
- 配置中心:管理员可编辑的回调地址也应视为不可信输入,不能因为有后台权限就跳过校验。
- 云环境:应用层阻断某个元数据地址并不能代替云平台侧的访问控制,应同时使用最小权限、元数据服务加固和网络隔离。
- 日志字段:记录完整 URL 时应注意查询参数中的令牌、签名和个人数据,必要时脱敏。
七、修复后的回归清单
修复完成后,建议将关键行为写成自动化测试,避免后续更换 HTTP 客户端或代理配置时重新引入问题。至少覆盖以下类别:
| 测试类别 | 预期结果 |
|---|---|
| 允许的业务域名与协议 | 正常请求,超时和响应大小受限 |
| 回环、私网、链路本地地址 | 在连接前拒绝 |
| IPv6 与 IPv4 映射地址 | 按统一地址策略拒绝或允许 |
| 非标准端口 | 按业务白名单处理,默认拒绝 |
| 重定向到受限地址 | 停止后续请求并记录原因 |
| 超时和超大响应 | 释放连接和资源,不阻塞任务线程 |
总结
SSRF 的审计重点不是寻找某一个“危险函数”,而是还原从用户输入到实际网络连接的完整链路。可靠的防护通常需要 URL 结构化解析、协议和端口白名单、DNS 地址校验、重定向控制、资源限制以及网络出口隔离共同完成。
从工程实践看,最有效的改进往往是减少任意 URL 能力:能使用固定服务列表就不要让用户输入完整地址,能使用资源标识就不要直接抓取远程内容。同时将拒绝日志、超时和响应大小纳入统一组件,避免每个业务团队各自实现一套不完整的校验逻辑。
推荐意见