服务端请求伪造(SSRF)并不是简单的“用户可控 URL”问题。真正决定风险的,通常是 URL 解析方式、DNS 解析行为、重定向处理、网络出口策略以及响应内容是否会返回给用户。很多修复只增加了一个内网 IP 判断,实际仍可能被 IPv6、十进制 IP、DNS 重绑定或重定向绕过。本文以代码审计和防守验证为主线,整理一套适用于 Java Web 服务的 SSRF 排查方法。示例均应在本地靶场或经授权的测试环境中进行,不直接访问生产内网、云平台元数据服务或其他未授权目标。一、常见业务场景与风险边界SSRF 通常出现在以下功能中:远程图片、头像、附件抓取;Webhook、回调地址或第三方通知配置;URL 预览、网页截图、Markdown 远程资源解析;导入远程 XML、JSON、RSS 或 OpenAPI 文档;代理下载、在线转码和远程健康检查。审计时不要只搜索 HttpClient 或 RestTemplate。更重要的是追踪数据流:用户输入是否进入 URL 构造函数,是否进入 DNS 解析、TCP 连接或 HTTP 客户端,响应是否被读取并返回给攻击者。判断 SSRF 的关键不是“是否发送了 HTTP 请求”,而是“请求的目标地址和请求内容是否被低可信输入影响,以及服务端网络权限是否超过了业务需要”。二、代码审计的入口与数据流可以先从高风险 API 入手,再回溯参数来源。Java 项目中常见的请求点包括:java.net.URL、java.net.URI;Apache HttpClient、OkHttp、WebClient、RestTemplate;文件下载工具、图片解析器和 HTML 渲染器;自定义的 download、fetch、proxy、callback 方法。建议将审计过程分为三步。第一步确认输入来源,例如 HTTP 参数、JSON 字段、数据库中的管理员配置或消息队列内容。第二步确认是否存在规范化和校验。第三步确认真正建立连接时使用的地址是否仍然来自原始输入。String target = request.getParameter("url");
URL url = new URL(target);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.setReadTimeout(5000);
InputStream input = connection.getInputStream();上面的代码存在多个审计疑点:没有限制协议;没有校验主机;没有处理重定向;DNS 解析和实际连接之间可能发生变化;读取响应时也没有限制大小。即使增加一个简单的字符串判断,也不能认为问题已经解决。三、URL 校验中最容易忽略的问题1. 不要使用字符串前缀判断以下做法都不可靠:if (url.startsWith(" {
// 允许访问
}字符串比较无法正确区分主机、端口、用户名信息和路径。例如,可信域名后面附加其他主机名、使用用户信息字段或改变大小写,都可能导致判断结果与实际连接目标不一致。应当先使用标准解析器解析,再对结构化字段进行校验。2. 只允许业务需要的协议如果业务只是抓取网页,通常只需要 http 和 https。应显式拒绝 file、jar、ftp、gopher 等不必要协议,同时注意底层 HTTP 客户端或第三方库是否支持自定义协议处理器。URI uri = URI.create(input);
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme) &&
!"https".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("unsupported scheme");
}3. 校验主机名,而不是只校验解析结果对于固定合作方,最稳妥的方式是使用允许列表,例如允许的完整域名、端口和路径范围。对于必须支持任意公网地址的场景,至少需要解析主机名并拒绝回环地址、链路本地地址、私有地址、保留地址和未指定地址。需要同时考虑 IPv4 和 IPv6。常见错误包括只判断 127.0.0.1,却没有覆盖 IPv6 回环地址;只判断字符串形式的私网网段,却没有处理不同格式的 IP 表示;只校验一次 DNS 结果,却在后续连接阶段重新解析。4. 注意 DNS 重绑定和解析时序典型的脆弱流程是:程序先解析域名并判断为公网地址,随后 HTTP 客户端再次解析同一域名并建立连接。如果两次解析结果不同,校验和连接使用的就不是同一个地址。防守侧可以采用以下策略:在解析阶段获取所有 A 和 AAAA 记录,逐一进行地址分类;连接时使用已经校验过的地址,避免再次根据原始域名解析;通过受控 DNS 解析器和网络出口策略做第二层限制;对重定向后的每个目标重新执行完整校验。在 Java 中,直接调用 InetAddress.getByName 并不能自动解决时序问题。关键在于校验后的地址是否真正被连接层使用,以及客户端是否会在内部重新解析域名。四、重定向是经常被遗漏的第二跳很多 HTTP 客户端默认跟随 301、302、307 或 308 重定向。如果只校验初始 URL,服务端可能先访问一个允许的站点,再被重定向到不应访问的地址。建议关闭自动重定向,由业务代码逐跳处理:HttpClient client = HttpClients.custom()
.disableRedirectHandling()
.setDefaultRequestConfig(
RequestConfig.custom()
.setConnectTimeout(3000)
.setResponseTimeout(5000)
.build())
.build();收到重定向响应后,应读取 Location,解析为绝对地址或基于原地址规范化后的新地址,然后重新执行协议、主机、端口和 IP 分类检查。重定向次数也应设置上限,通常一到三次已经足够。五、推荐的防护分层SSRF 不应只依赖一段业务代码修复,建议建立多层控制。第一层:业务允许列表能使用固定目标列表时,不要开放任意 URL。允许列表应至少包含协议、主机、端口,必要时还应限制路径。域名匹配要使用完整标签边界,不能简单使用后缀匹配。boolean allowedHost(String host) {
return "api.partner.example".equalsIgnoreCase(host)
|| "storage.partner.example".equalsIgnoreCase(host);
}第二层:地址分类与连接前校验对域名解析得到的每个地址进行分类,拒绝回环、私有、链路本地、组播、保留和未指定地址。还要限制端口范围,避免业务只需要 443 却允许访问任意内部服务端口。第三层:HTTP 客户端限制禁用不需要的协议和重定向;设置连接、读取和连接池超时;限制响应头和响应体大小;限制单次请求的最大下载时间;禁止将内部请求凭据自动转发到外部目标。第四层:网络出口控制在容器、虚拟机或主机防火墙层面限制服务的出站访问,只允许业务所需的 DNS、代理和目标网段。网络策略是代码防护失效后的最后一道边界,也能降低 SSRF 进一步访问内部管理面板、消息队列和数据库的风险。六、一个更完整的校验思路下面的代码是审计和设计时的简化示意,不建议未经评估直接复制到生产环境。生产实现还需要处理 DNS 缓存、IPv6、代理配置、连接复用和客户端内部解析行为。URI uri = URI.create(input);
if (uri.getUserInfo() != null) {
throw new IllegalArgumentException("userinfo is not allowed");
}
String scheme = uri.getScheme();
if (!"http".equalsIgnoreCase(scheme)
&& !"https".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("scheme is not allowed");
}
String host = uri.getHost();
if (host == null || host.length() == 0) {
throw new IllegalArgumentException("invalid host");
}
int port = uri.getPort();
if (port != -1 && port != 80 && port != 443) {
throw new IllegalArgumentException("port is not allowed");
}
for (InetAddress address : DnsResolver.resolveAll(host)) {
if (AddressPolicy.isPrivateOrReserved(address)) {
throw new IllegalArgumentException("address is not allowed");
}
}实际连接时,还要确保 HTTP 客户端使用的是已校验的解析结果,或者通过受控代理统一发起请求。若无法保证客户端不会重新解析域名,应优先采用出口代理方案,并在代理层再次执行目标策略。七、响应处理同样需要安全边界SSRF 风险不只在“请求发到哪里”,还在于响应如何被处理。建议关注以下问题:限制响应体大小,防止大文件导致内存或磁盘耗尽;不要直接把内部服务响应原样返回给前端;按业务需要限制响应内容类型,图片抓取不应接受任意脚本或可执行格式;下载到临时目录时使用随机文件名,并设置权限和生命周期;对压缩文件、XML 和图片解析设置资源限制,避免引入其他解析类漏洞;记录目标域名、最终地址、状态码、响应大小和耗时,但不要记录敏感响应正文。如果业务只是判断 URL 是否可用,可以只读取响应头或少量内容,不必将整个响应加载到内存。对于异步抓取任务,应增加任务级超时、并发上限和失败重试上限。八、审计与验证清单检查项关注点输入来源参数、JSON、数据库配置、消息队列是否可被低可信用户影响协议限制是否只允许 http/https,是否存在自定义协议处理器主机校验是否使用结构化解析,是否覆盖 IPv4、IPv6 和域名DNS 行为校验和连接是否可能发生二次解析,是否考虑解析结果变化重定向是否关闭自动跟随,跳转后的目标是否重新校验端口控制是否限制到业务需要的端口,是否存在任意端口访问响应处理是否限制大小、类型、超时,并避免直接回显内部内容网络策略服务是否具备不必要的内网和管理网段出站权限九、在测试环境中的验证方法验证时建议搭建两个本地服务:一个作为允许访问的正常站点,另一个作为重定向站点或记录请求的观察端。测试目标是确认校验逻辑是否按预期拒绝不允许的协议、地址、端口和跳转,而不是扫描真实内网。python3 -m http.server 8080 --bind 127.0.0.1可以在单元测试中覆盖以下类别:合法公网域名、明确的本地回环地址、IPv6 回环地址、私有地址、无效端口、带用户名信息的 URL、初始地址合法但重定向地址不合法的场景,以及超大响应和长时间不返回的场景。测试结果应验证请求是否被阻断、日志是否完整、服务是否能够及时释放连接。十、日志、告警与事件复盘对可能发起外部请求的服务,建议记录结构化审计日志,包括请求来源、业务接口、目标主机、解析地址、最终地址、端口、状态码、耗时、响应大小和拒绝原因。日志中的查询参数和响应内容应进行脱敏。告警可以关注短时间内大量不同目标、访问异常端口、频繁触发地址策略拒绝、请求耗时明显升高以及服务向不常见公网网段发起连接等情况。复盘时要同时检查代码、依赖库默认行为、容器网络策略和云环境出站权限,避免只修补某一个 URL 参数。总结SSRF 的有效防护需要把 URL 解析、DNS、连接建立、重定向、响应处理和网络出口看成一条完整链路。单独增加黑名单、判断字符串前缀或只拦截某个内网地址,都很难覆盖真实环境中的边界情况。更可靠的实践是:优先使用业务允许列表;严格限制协议、主机和端口;对所有解析结果执行地址分类;关闭自动重定向并逐跳校验;限制响应资源;最后通过代理和网络策略建立出口边界。代码审计时沿着“输入来源—URL 构造—解析—连接—响应”的数据流检查,通常比单纯搜索危险函数更容易发现完整问题。
推荐意见