SSRF(Server-Side Request Forgery,服务端请求伪造)并不是一个只靠“拦截内网 IP”就能解决的问题。实际审计中,风险通常出现在图片抓取、Webhook、URL 预览、文档转换、第三方接口探测等功能里。应用一旦允许用户影响服务端发起网络请求,就需要同时考虑协议限制、地址解析、重定向、DNS 变化、响应大小和网络出口控制。
一、问题背景与典型场景
常见代码模式是接收一个 URL,然后由后端下载内容并返回给用户。例如:
String target = request.getParameter("url");
URL url = new URL(target);
URLConnection connection = url.openConnection();
InputStream input = connection.getInputStream();
这段代码的危险不在于 Java API 本身,而在于 target 完全由外部输入控制。攻击者可能让服务端访问不应该暴露的管理接口、云环境元数据服务、内部调试端口,或者借助重定向绕过初始校验。
从防守角度看,审计重点不是寻找某一个特殊 payload,而是确认以下问题:
- 请求协议是否限制为 HTTPS 或明确允许的协议。
- 目标主机是否采用严格的业务白名单。
- 域名解析后的所有地址是否经过校验。
- 连接建立后是否还会发生重定向。
- DNS 解析结果与实际连接目标是否可能不一致。
- 是否设置连接超时、读取超时和响应大小上限。
- 服务器网络层是否阻断了不必要的内网出口。
二、代码审计中的分析路径
第一步是从网络请求入口开始追踪数据流。搜索参数名并不可靠,建议同时关注 URL、URI、host、callback、webhook、image、avatar、source、fetch 等字段,再沿着调用链定位真正发起网络请求的位置。
第二步要区分“字符串校验”和“网络连接”。很多修复只对原始字符串做判断,例如禁止出现 127.0.0.1 或 localhost,但 URL 可以使用域名、IPv6、十进制整数形式的 IPv4 地址,或者通过重定向指向不同目标。因此需要以解析后的网络地址为准进行决策。
第三步检查 HTTP 客户端的默认行为。部分客户端默认跟随 3xx 重定向、自动解压响应、复用连接或允许多种协议。如果业务只需要下载固定类型的公开资源,默认行为通常过于宽松。
三、推荐的防护策略
1. 优先使用业务白名单
如果功能只需要从几个固定的对象存储域名下载图片,最稳妥的方案是维护精确的主机名白名单,并限制协议、端口和路径。黑名单只能作为补充,不能作为主要防线。
允许的协议:HTTPS
允许的主机:cdn.example.test、assets.example.test
允许的端口:443
允许的响应类型:image/png、image/jpeg
单次响应上限:5 MB
主机名比较应采用规范化后的结果,避免大小写、尾部点号和端口表示差异造成策略绕过。对于国际化域名,还应明确使用 IDNA 规范化后的形式进行比较。
2. 解析后校验所有 IP 地址
在无法使用严格白名单时,至少要对目标域名解析出的每一个地址进行检查,拒绝回环地址、私有地址、链路本地地址、未指定地址、组播地址以及业务明确不需要的保留地址。不要只检查一次 DNS 结果后就无限期复用。
URI uri = URI.create(input);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw new IllegalArgumentException("unsupported scheme");
}
if (uri.getUserInfo() != null || uri.getHost() == null) {
throw new IllegalArgumentException("invalid authority");
}
InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
for (InetAddress address : addresses) {
if (address.isAnyLocalAddress()
|| address.isLoopbackAddress()
|| address.isLinkLocalAddress()
|| address.isSiteLocalAddress()
|| address.isMulticastAddress()) {
throw new IllegalArgumentException("private or reserved address");
}
}
上面的判断适合作为示意,生产环境还应补充企业网段、云厂商保留网段和 IPv6 特殊地址的策略。不同语言和库对“私有地址”的定义并不完全一致,不能只依赖一个便捷方法。
3. 禁止自动跟随重定向
即使初始 URL 通过了检查,服务端也可能收到 301、302、307 或 308 响应并继续请求另一个地址。建议关闭自动重定向,由业务代码逐跳检查新的 Location,并限制最大跳转次数。
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NEVER)
.connectTimeout(Duration.ofSeconds(3))
.build();
处理 Location 时,应重新执行协议、主机、端口和解析地址校验,不能把它当作普通字符串直接拼接或复用原有连接配置。
4. 限制请求规模和资源消耗
SSRF 防护不能只关注“能不能访问”,还要关注“访问会消耗多少资源”。应设置连接超时、读取超时、最大响应体、最大响应头、最大重定向次数和并发限制。对于图片或文档抓取,还要在服务端根据 Content-Type、文件头和解码后的尺寸再次校验,避免压缩炸弹和超大图片导致内存异常。
四、DNS 重绑定与网络层防护
如果应用先解析域名做安全判断,随后又由底层客户端独立解析并建立连接,就可能出现两次解析结果不同的问题。攻击者控制的域名可以在短时间内改变 DNS 响应,使校验阶段返回公网地址,而连接阶段返回内部地址。
应用层可以通过固定解析结果、使用受控 DNS 解析器、在连接建立前后再次确认目标地址等方式降低风险,但不同 HTTP 客户端对底层连接的控制能力不同。更可靠的做法是将应用层校验与网络出口策略结合起来:
- 在容器或主机防火墙层禁止访问云元数据地址和不必要的内网网段。
- 让抓取服务运行在独立网络区域,只允许访问明确的代理或外部出口。
- 通过 egress proxy 统一执行域名、地址、端口和审计策略。
- 对异常目标、频繁失败、连接内网地址等行为记录安全日志。
应用层校验解决“业务是否允许请求”,网络层隔离解决“即使校验失效,服务端还能访问到什么”。两者不应互相替代。
五、修复验证与测试用例
修复后不要只测试一个正常 URL。建议建立自动化测试矩阵,覆盖协议、地址族、重定向和异常输入。测试目标可以使用本地模拟服务或专用测试域名,不需要访问真实内部系统。
| 测试类别 | 示例 | 预期结果 |
|---|---|---|
| 正常请求 | 白名单 HTTPS 主机 | 允许,并记录目标主机 |
| 协议限制 | HTTP、file、gopher 等非业务协议 | 拒绝 |
| 地址限制 | 回环、私有、链路本地和保留地址 | 拒绝 |
| 重定向 | 公开地址重定向到不允许地址 | 在跳转前拒绝 |
| 资源限制 | 超大响应、慢响应、多次跳转 | 超时或中止 |
| 解析异常 | 无效域名、IPv6、空主机名 | 安全失败 |
在测试环境中可以启动一个仅绑定本机的模拟 HTTP 服务,用于验证超时、响应大小和重定向处理:
python3 -m http.server 8000 --bind 127.0.0.1
这里的重点是验证应用是否按预期拒绝不允许的目标,而不是通过真实内网探测来“证明漏洞”。测试日志中应保留请求 ID、规范化后的主机、解析地址、最终状态和拒绝原因,但不要记录敏感响应正文。
六、常见错误修复方式
- 只拦截字符串 127.0.0.1:无法覆盖域名解析、IPv6 和其他地址表达形式。
- 只校验 URL 的 hostname:未检查解析结果,也未处理重定向。
- 使用正则判断 URL:容易忽略用户信息、端口、编码和规范化问题。
- 校验通过后长时间缓存 DNS:会放大地址变化带来的风险。
- 只依赖云平台元数据服务的访问令牌:这属于纵深防御,不是应用层授权校验的替代品。
- 修复后没有限制响应体和超时:即使不能访问内网,仍可能形成资源消耗问题。
七、总结
SSRF 的本质是将服务端的网络位置和权限暴露给了不可信输入。可靠治理需要从数据流审计开始,结合协议与主机白名单、解析后地址校验、重定向控制、资源限制、日志监控和网络出口隔离。对于高风险功能,建议把“任意 URL 抓取”改造成“业务资源 ID 获取”,从设计上减少用户直接控制网络目标的机会。
最终验收时,应同时检查代码路径、HTTP 客户端配置、容器网络策略和自动化测试结果。只有当应用层和基础设施层都具备明确的拒绝边界,修复才不容易因库升级、配置变化或新增功能而再次失效。
推荐意见