SSRF(服务端请求伪造)在实际项目中并不只表现为一个“URL 参数可控”。真正影响风险等级的,通常是请求发起位置、协议处理方式、重定向行为、DNS 解析时机,以及服务器本身能够访问哪些内部资源。本文以代码审计和防守验证为主线,整理一套适用于 Java Web 服务的 SSRF 分析方法。
一、问题背景:为什么 SSRF 容易被低估
很多业务都需要由服务端访问外部地址,例如抓取网页标题、下载远程图片、校验回调地址、生成预览内容或调用第三方接口。开发人员往往只做了字符串层面的限制,例如禁止出现 127.0.0.1,却没有考虑以下情况:
- 域名解析结果可能指向内网地址。
- 应用可能自动跟随 30x 重定向。
- IPv6、十进制 IPv4、IPv4 映射地址等表示方式可能绕过简单黑名单。
- URL 中的用户名、端口、片段和编码字符可能影响解析结果。
- 应用所在容器、云主机或办公网络本身拥有额外的内网访问权限。
因此,审计 SSRF 时不能只问“用户能否控制 URL”,还要继续确认“服务端最终连接到了哪里,以及连接后能做什么”。
二、审计入口:先定位服务端请求发起点
代码审计时,可以先从常见的网络请求 API 入手,再反向追踪 URL 的数据流。Java 项目中常见的请求发起点包括:
java.net.URL、HttpURLConnection。- Apache HttpClient、OkHttp、Spring WebClient、RestTemplate。
- 文件下载、图片代理、网页截图、Webhook 校验等业务封装。
- XML 外部实体、模板渲染器或第三方 SDK 中隐藏的远程资源加载逻辑。
建议建立如下数据流记录:输入参数、URL 拼接或解析位置、协议限制、DNS 解析位置、连接建立位置、重定向策略、响应处理方式,以及异常分支。尤其要关注“看起来已经校验过 URL,但后续又把原始字符串交给另一个组件”的情况。
三、典型风险代码与问题拆解
下面是一段简化的示例代码。它的问题不在于使用了某一个特定类,而在于把校验和实际连接拆成了两个不一致的阶段。
public byte[] fetch(String rawUrl) throws IOException {
if (rawUrl.startsWith(" || rawUrl.startsWith(" {
if (rawUrl.contains("127.0.0.1") || rawUrl.contains("localhost")) {
throw new IllegalArgumentException("blocked");
}
}
URL url = new URL(rawUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.setReadTimeout(5000);
return connection.getInputStream().readAllBytes();
}
这段实现至少存在以下问题:
- 使用字符串包含判断代替了规范化后的主机地址判断。
- 没有限制端口,可能访问服务端不希望暴露的管理接口。
- 没有明确关闭重定向,实际行为依赖客户端默认配置。
- 没有限制响应大小,可能导致内存消耗或磁盘写入风险。
- 连接建立时的 DNS 解析与前置校验之间存在时序差异。
- 没有限制响应内容类型,业务层可能把远程内容当作 HTML、XML 或脚本继续处理。
四、正确的分析思路:校验最终连接目标
防御的核心不是维护一个越来越长的黑名单,而是对目标进行规范化、解析和策略判断。一个较稳妥的处理流程如下:
- 只接受业务确实需要的协议,通常为 HTTP 和 HTTPS。
- 使用标准 URI 解析器提取主机、端口和协议,拒绝模糊或异常格式。
- 对主机名进行 DNS 解析,逐个检查所有 A 和 AAAA 记录。
- 拒绝回环地址、链路本地地址、私有地址、保留地址和多播地址。
- 明确允许的端口,避免把 HTTP 请求能力扩展为任意 TCP 探测。
- 默认关闭跨域重定向;如果业务必须支持重定向,每一跳都重新执行完整校验。
- 在网络层增加出口访问控制,代码校验不能替代防火墙和云网络策略。
五、示例:一个更安全的目标校验框架
下面的代码仅展示审计时应关注的结构,实际项目还需要结合代理配置、IPv6 环境和 DNS 服务特性进行测试。
public URI validateTarget(String raw) throws Exception {
URI uri = URI.create(raw);
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
throw new SecurityException("unsupported scheme");
}
if (uri.getUserInfo() != null || uri.getHost() == null) {
throw new SecurityException("invalid authority");
}
int port = uri.getPort();
if (port != -1 && port != 80 && port != 443) {
throw new SecurityException("port not allowed");
}
InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
for (InetAddress address : addresses) {
if (isNonPublicAddress(address)) {
throw new SecurityException("non-public address");
}
}
return uri;
}
isNonPublicAddress 不应只依赖 isSiteLocalAddress()。在生产实现中,需要同时覆盖 IPv4 和 IPv6 的回环、链路本地、私有、未指定、组播及其他保留地址。对于云环境,还应根据实际网络架构单独限制云平台元数据地址和内部解析域名。
六、DNS 重绑定与“校验一次、连接一次”问题
即使代码先解析域名并判断其为公网地址,后续连接时仍可能再次解析该域名。如果两次解析结果不同,就会出现校验阶段和连接阶段不一致的问题。这类风险通常被称为 DNS 重绑定或 TOCTOU 问题。
审计时可以重点确认:
- 校验使用的解析结果是否被实际连接复用。
- HTTP 客户端是否会自行重新解析域名。
- 是否允许用户控制 DNS 服务器、代理地址或自定义 Host。
- 连接建立后是否还会因为重定向、代理或连接池复用而访问其他目标。
较理想的实现是使用统一的解析和连接控制层,在连接建立前后都验证目标地址,并在网络出口层阻断内网网段。对于高风险业务,可以把远程抓取功能放到独立的沙箱或隔离网络中,避免主业务进程直接拥有内网访问能力。
七、重定向、代理与协议处理
重定向是审计中经常遗漏的一环。初始地址可能是公开站点,但响应返回的 Location 指向内部地址。如果客户端自动跟随重定向,前置校验就失去了意义。
建议采取以下策略:
- 默认关闭自动重定向,并在业务层明确决定是否允许。
- 必须跟随时,限制跳转次数,并对新的 URL 重复执行协议、端口、解析地址检查。
- 禁止从 HTTP 跳转到不符合业务要求的协议。
- 确认系统代理环境变量不会让请求绕过应用自身的目标校验。
- 不要把用户可控的 URL、Host 头或代理配置直接传给底层客户端。
八、响应处理同样决定风险
SSRF 不只是“能不能发出请求”。如果服务端会把响应原样返回给用户,或者继续解析远程内容,风险面会进一步扩大。
- 限制响应体大小,读取时使用上限计数,避免直接调用无界的读取方法。
- 限制连接、读取和整体请求超时时间。
- 根据业务白名单限制
Content-Type,不要把 XML、脚本或可执行文件交给后续解析器。 - 下载文件时使用随机临时文件名,并校验文件类型、大小和存储位置。
- 错误信息中不要返回内部 IP、完整请求头或远程服务的敏感响应。
- 记录目标域名、解析地址、状态码、响应大小和耗时,便于事后审计。
九、安全验证:在本地构造可控测试环境
验证 SSRF 防护时,建议使用本地模拟服务或专门的测试网段,不要直接探测生产内网。可以启动一个只监听本机高位端口的测试服务,用于观察客户端是否访问、是否跟随重定向以及是否正确限制响应大小。
python3 -m http.server 18080 --bind 127.0.0.1
测试项可以按以下顺序执行:
- 正常公网 HTTPS 地址是否能够按业务预期访问。
- 本地回环地址是否被拒绝。
- 解析到测试保留网段的域名是否被拒绝。
- 非允许端口是否被拒绝。
- 重定向到禁止目标时是否停止。
- 超大响应、慢响应和错误证书是否按预期处理。
- IPv4 和 IPv6 环境下的判断结果是否一致。
测试结果应记录为可重复的用例,而不是只保留一次手工验证截图。对于代码审计报告,最好同时给出输入点、调用链、触发条件、实际网络边界和修复后的回归结果。
十、常见修复误区
- 只禁止几个字符串:这无法覆盖解析差异和地址表示差异。
- 只校验域名后缀:攻击者可能控制同名子域、DNS 记录或跳转目标。
- 只依赖应用层判断:没有出口 ACL 时,代码缺陷可能直接暴露内部网络。
- 只修复一个接口:下载、预览、Webhook、图片处理等功能可能共用另一套请求封装。
- 只关注响应内容:请求本身对内部服务造成的状态变化也可能是风险。
- 修复后没有回归重定向和 IPv6:这两类问题经常在补丁中被遗漏。
十一、落地建议
对于普通业务,建议将远程请求封装成统一组件,禁止各模块自行拼接和发起请求。组件至少应统一处理协议、域名解析、地址分类、端口、重定向、超时、响应大小和日志字段。
对于高风险场景,例如允许用户提交回调地址、抓取任意网页或处理第三方文件,建议采用分层控制:
- 应用层使用严格的目标校验和响应限制。
- 网络层禁止访问云元数据、管理网段和不必要的内部服务。
- 运行层使用独立账号、容器或沙箱,减少本地文件和凭据暴露。
- 监控层对异常目标、短时间大量失败请求、内网地址命中和异常响应大小进行告警。
总结
SSRF 审计的关键不是寻找一个可以绕过的黑名单,而是完整还原“用户输入如何变成一次网络连接”。从输入点、URI 解析、DNS 解析、连接建立,到重定向、响应处理和网络出口,每一层都可能改变最终风险。
在修复上,代码校验、请求组件封装和网络出口隔离应当同时存在。只有把“不能访问不可信目标”落实为可验证的代码规则和基础设施策略,才能避免修复停留在字符串过滤层面。
推荐意见