SSRF(服务端请求伪造)并不是“让服务器代替用户发一个 HTTP 请求”这么简单。真正需要关注的是:哪些输入能够影响出站请求、服务器能够访问哪些网络区域、请求是否会跟随重定向、解析结果是否可被重新绑定,以及响应内容是否会进一步进入业务流程。
在实际审计中,SSRF 往往隐藏在图片抓取、Webhook、文档转换、URL 预览、远程文件导入和健康检查等正常功能里。单纯搜索 http.get 或 requests.get 容易漏报,也容易把正常的固定地址请求误判为漏洞。比较可靠的方法是围绕“输入—解析—连接—响应处理”建立完整的数据流。
一、先明确场景和威胁边界
审计前应先回答三个问题:
- 用户是否能够控制完整 URL,或者至少控制主机名、端口、路径中的一部分?
- 服务端所在网络是否拥有浏览器或普通用户无法获得的访问权限?
- 请求响应是否会被返回给用户、写入文件、进入解析器,或触发后续业务动作?
这三个问题分别对应输入控制、网络能力和影响面。即使响应不会直接回显,只要请求能够触发内部服务的状态变化,或者造成敏感信息泄露、日志污染、资源消耗,也需要纳入风险评估。
在授权的测试环境中,建议使用本地 mock 服务模拟不同响应,不要直接探测真实内网、云平台管理地址或第三方系统。这样既能验证代码逻辑,也能避免测试对生产环境造成影响。
二、从数据流而不是函数名入手
建议沿着以下路径进行代码审计:
- 定位输入源:HTTP 参数、JSON 字段、数据库记录、消息队列、配置文件和导入文件都可能成为间接输入。
- 跟踪规范化逻辑:关注 URL 解码、大小写转换、默认端口补全、路径清理和主机名提取。
- 确认连接实现:包括 HTTP 客户端、代理配置、重定向策略、DNS 解析方式和连接超时。
- 分析响应处理:判断响应是否会原样返回、落盘、解析为 XML/HTML,或被传递给其他组件。
- 核对边界控制:检查是否存在主机白名单、协议限制、私网地址拦截、端口限制和出站网络策略。
常见的高风险代码形态如下,问题不在于使用了某个特定库,而在于未经约束地把外部输入交给网络客户端:
String target = request.getParameter("url");
String body = httpClient.get(target);
return body;审计时不要只看控制器。还要继续追踪 httpClient 的默认行为,例如是否自动跟随 3xx、是否使用环境代理、是否允许非 HTTP 协议、是否拥有较长的读取超时。
三、URL 校验最容易出现的误区
1. 只检查字符串前缀
类似“以某个域名开头”或“包含某个关键字”的判断不具备可靠的主机边界。URL 语法中存在用户信息、端口、路径、编码和大小写等多个部分,字符串判断很容易把路径或用户名误当成主机名。
正确做法是先使用标准 URL 解析器拆分结构,再对协议、主机名、端口和路径分别执行策略判断。不要自行用正则表达式实现完整 URL 解析。
2. 只校验一次 DNS 解析结果
如果应用先解析域名并确认地址属于公网范围,随后网络库又重新解析一次域名,就可能产生校验和连接目标不一致的问题。这类问题本质上是校验与使用之间存在时间窗口。
更稳妥的设计是让“最终建立连接的地址”也经过策略控制。对于高风险场景,可以使用固定的出口代理,由代理统一完成解析、地址分类和连接;也可以在底层连接器中复用经过校验的解析结果。仅在业务层调用一次 resolve 并不能自动解决这个问题。
3. 忽略 IPv6 和特殊地址
很多项目只判断 IPv4 私网段,却没有覆盖 IPv6 的本地地址、链路本地地址、回环地址和映射地址。地址分类应使用成熟的网络库,同时明确处理解析返回的全部地址,而不是只取第一条。
4. 只限制初始 URL,不限制重定向
初始地址符合白名单,并不代表重定向后的地址也符合。安全配置通常应默认关闭跨主机自动重定向;如果业务必须支持重定向,应逐跳重新执行协议、主机、端口和地址策略,并限制最大跳数。
四、一个更稳妥的防护策略
SSRF 防护不应依赖单一检查,而应采用分层控制:
| 层次 | 建议 | 目的 |
|---|---|---|
| 业务层 | 优先使用资源 ID 或固定枚举,不直接接收任意 URL | 从源头减少可控范围 |
| 协议层 | 仅允许明确需要的协议,例如 HTTPS | 避免非预期协议处理 |
| 主机层 | 使用精确域名白名单,避免模糊后缀匹配 | 限制访问目标 |
| 地址层 | 拒绝回环、私网、链路本地、组播和保留地址 | 降低访问内部资源的可能性 |
| 连接层 | 关闭或严格控制重定向,限制端口、连接数和超时 | 防止策略被绕过或资源耗尽 |
| 网络层 | 通过防火墙、出口代理和 egress ACL 限制服务器出站范围 | 即使应用失效也保留最后一道边界 |
如果业务只是抓取合作方的固定资源,优先采用资源标识映射方式:客户端提交一个业务 ID,服务端从受控配置中取得对应 URL。相比让客户端提交完整地址,这种设计更容易审计,也更容易进行审计留痕和变更管理。
五、实现时应关注的细节
下面是一段表达防护思路的伪代码,重点是处理顺序,不应直接当作通用库使用:
URI uri = parseByStandardLibrary(input);
if (!isAllowedScheme(uri.getScheme())) {
reject();
}
String host = normalizeHostname(uri.getHost());
if (!isAllowedHost(host)) {
reject();
}
List<IPAddress> addresses = resolver.resolveAll(host);
for (IPAddress address : addresses) {
if (isLoopback(address) || isPrivate(address)
|| isLinkLocal(address) || isReserved(address)) {
reject();
}
}
client.setFollowRedirects(false);
client.setConnectTimeout(Duration.ofSeconds(3));
client.setReadTimeout(Duration.ofSeconds(5));
client.setMaxResponseBytes(2 * 1024 * 1024);
return client.fetch(uri);这里有几个容易被忽视的点:
- 主机名规范化:需要处理大小写、末尾点、国际化域名和解析器返回的标准形式。白名单比较应基于规范化后的主机名。
- 端口控制:如果业务只需要 HTTPS,通常不应开放任意端口。即使端口属于公网,也可能连接到不符合预期的管理服务。
- 响应大小:限制最大响应体可以减少内存消耗和磁盘写入风险。对于流式处理,也要限制总读取时间和总字节数。
- 内容类型:不要因为响应体看起来像图片或 JSON 就直接交给解析器。应同时检查声明的 Content-Type、文件特征和解析器的安全配置。
- 代理环境:确认 HTTP 客户端是否会读取系统环境变量中的代理设置,避免开发环境与生产环境行为不一致。
六、测试与验证方法
在测试环境中,可以搭建两个本地 mock 服务:一个返回正常业务内容,另一个用于模拟重定向、超时、超大响应和异常 Content-Type。测试重点不是“能否访问某个内部地址”,而是验证策略是否在各种输入变形下保持一致。
建议至少覆盖以下用例:
- 缺少协议、大小写混合、末尾点和国际化域名。
- 显式端口、非默认端口和不支持的协议。
- 解析到多个地址,其中一个地址不符合策略。
- 初始地址合法但重定向目标不合法。
- 连接超时、读取超时、响应体超过限制。
- 服务端返回压缩响应、错误 Content-Type 或畸形头部。
- 请求参数经过多次 URL 编码、JSON 编码或业务层二次解码。
单元测试应覆盖 URL 解析和地址分类,集成测试则验证真实 HTTP 客户端的重定向、代理、超时和连接池行为。很多漏洞并不是校验函数本身错误,而是校验函数和实际连接器之间存在行为差异。
七、日志、告警与事件复盘
防护上线后,还需要能够回答“谁在什么时候请求了什么目标,以及最终连接到了哪里”。建议记录以下字段:
- 请求来源的业务用户或服务身份,而不是只记录 IP。
- 原始输入的脱敏摘要和规范化后的主机名。
- 解析得到的地址类别、最终端口、是否发生重定向。
- 阻断原因、连接耗时、响应大小和结果状态。
日志中不要直接写入包含敏感参数的完整 URL,也不要把远端响应原样写入日志。对于连续触发地址策略、超时或超大响应限制的行为,可以结合业务身份进行限速和告警,但不建议只依赖单一 IP 阈值。
事件复盘时,应同时检查应用代码、容器网络策略、云安全组、出口代理和凭据权限。SSRF 的实际影响通常取决于这些外围条件,而不仅是某个接口是否存在输入校验缺陷。
总结
高质量的 SSRF 审计应从数据流和网络边界出发,而不是停留在搜索危险函数。有效的防护组合通常包括:减少任意 URL 输入、使用标准解析器、执行协议和主机白名单、完整处理 IPv4/IPv6 地址、控制重定向与资源消耗,并在网络层限制出站访问。
最后,代码层校验始终不应被视为唯一防线。应用策略、连接器行为和基础设施出站控制需要一起验证,才能让漏洞修复从“看起来加了判断”真正变成可持续、可审计的安全控制。
推荐意见