Java 原生的 HttpURLConnection 在发起请求时,有一个极易被开发者忽视的默认行为:当接收到服务端返回的 3xx 重定向响应时,它会自动、静默地向新的 Location 地址发起请求。这个看似“人性化”的特性,在涉及用户可控 URL 的场景下,会直接导致严重的服务器端请求伪造漏洞。攻击者只需构造一个对外可见的 HTTP 302 跳转,就能让后端 Java 应用乖乖地去请求内网地址、云实例元数据接口、甚至本地敏感文件协议。
默认跟随重定向的底层机制HttpURLConnection 的实例方法 getInputStream() 和 getResponseCode() 在底层实现中,会隐式触发对重定向的处理。具体逻辑封装在 sun.net.www.protocol.http.HttpURLConnection 的 followRedirect 方法中。一旦响应码是 301、302、303、307 或 308,并且 Location 头部不为空,JDK 就会自动构造一个新的连接,向新地址发起请求,直到达到最大重定向次数或者遇到非重定向响应为止。整个过程对上层调用者完全透明,开发者拿到的已经是最终响应,完全不知道中间发生了跳转。
静态方法 setFollowRedirects 的全局陷阱HttpURLConnection 提供了一个静态方法 setFollowRedirects(boolean),它影响的是所有新创建的连接实例。如果项目中某处为了省事调用了 HttpURLConnection.setFollowRedirects(false) 来全局禁用重定向,确实能堵住漏洞,但会破坏所有依赖自动重定向的业务逻辑。更危险的是,这个设置是 JVM 级别的全局状态,在多线程环境下可能被其他代码意外修改,导致不可预期的行为。因此,不推荐使用静态方法来做安全控制。
实例级禁用才是正确姿势正确的做法是针对每一个连接实例单独设置重定向策略。通过调用实例方法 setInstanceFollowRedirects(false),可以精确控制当前连接不跟随重定向,而不会影响其他连接。当设置此参数为 false 后,程序需要自行处理 3xx 响应码,手动解析 Location 头部,并在验证目标地址安全性之后再决定是否发起第二次请求。这样就把重定向的控制权从 JDK 交回到了开发者手中。
URL url = new URL(userInputUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode >= 300 && responseCode < 400) {
String location = connection.getHeaderField("Location");
// 必须对 location 进行白名单校验,禁止内网地址
if (isSafe(location)) {
// 手动发起新请求
} else {
throw new SecurityException("Blocked redirect to: " + location);
}
}
302 跳转如何绕过常见的主机名校验
很多开发者会在第一次请求前对用户提供的 URL 做域名解析,判断,检查目标 IP 是否属于内网段。这种校验在自动跟随重定向.redirect 的场景下形同虚设。攻击者只需要在自己的服务器上部署一个重定向端点,当 Java 程序请求 http://evil.com/redirect 时,服务端返回 302 并指定 Location 为 http://169.254.169.254/latest/meta-data/,JDK 的自动重定向机制会直接向云元数据地址发起第二次请求,完全绕过第一次的 IP 校验。因为第二次请求是 JDK 内部发起的,开发者根本没有机会介入校验。
协议走私风险:从 HTTP 到 file 协议更隐蔽的攻击方式是利用重定向进行协议切换。如果用户提交的初始 URL 是 http://attacker.com/jump,服务端返回的 Location 可以是 file:///ete/passwd 或者 netdoc:/// 这样的本地协议地址。虽然较新版本的 JDK 对 followRedirect 做了一定限制,不允许从 http 协议跳转到非 http 协议,但在某些旧版本或者特定实现中,这种跨协议重定向仍然可能生效。攻击者还可以利用 gopher 协议在旧版本 JDK 中构造盲 SSRF 请求。因此,除了禁用自动重定向,还必须在手动处理重定向时严格限制目标协议,只允许 https 或 http。
DNS 重绑定与 TOCTOU 竞态即使开发者在第一次请求前做了 DNS 解析和 IP 校验,攻击者仍然可以利用 DNS 重绑定技术绕过。攻击者的域名在第一次解析时返回一个合法的外网 IP,通过校验,但在重定向后的第二次请求时,DNS 解析返回内网 IP。由于 JDK 默认的缓存机制,这种攻击有一定难度,但在 TTL 设置极短的情况下是可行的。更可靠的方案是,在手动发起重定向请求前,对目标 URL 重新做一次完整的校验,包括域名解析和 IP 检查,而不是依赖第一次校验的结果。
第三方 HTTP 库的类似风险不仅 HttpURLConnection 存在此问题,Apache HttpClents、OkHtp 等流行 HTTP 库在默认配置下同样会自动跟随重定向。以 OkHtp 为例,其默认的 followRedirects 为 true,且 followSslRedirects 也为 true,意味着它甚至会跟随从 HTTPS 到 HTTP 的降级重定向。Apache HttpClents 的 LaxRedirectStrategy 默认允许重定向,且对跨协议重定向的限制较为宽松。在使用这些库时,必须显式关闭自动重定向,或者实现自定义的重定向策略,在每次重定向时对目标地址进行安全校验。
SSRF 到内网服务的纵深利用一旦攻击者能够控制服务端发起内网 HTTP 请求,危害远不止读取云元数据。很多企业内部服务没有鉴权,比如 Redis、Memcahed、Elastisearch、Doker API 等,都监听在本地或内网地址上。通过精心构造的 URL 路径和参数,攻击者可以对这些服务进行未授权操作。例如利用 HTTP 请求向 Redis 发送命令,通过 CRLF 注入在 HTTP 头部中嵌入 Redis 协议指令。虽然 HttpURLConnection 对请求格式有一定# 有一定限制,但结合重定向和特定端点的特性,仍然存在利用可能。
防御方案的多层纵深第一层,对所有用户提供的 URL 进行严格的白名单校验,只允许访问特定域名或 IP 段。第二层,在所有 HTTP 连接实例上禁用自动重定向,由代码手动处理重定向逻辑。第三层,在每次重定向时重新解析目标地址,检查 IP 是否属于内网、保留地址或云元数据地址。第四层,限制允许的协议仅为 HTTP 和 HTTPS,禁止 file、jar、gpher 等危险协议。第五层,在网路出口配置防火墙规则,禁止服务端向内部敏感网段发起连接,作为最后一道防线。多层防御即使某一层被绕过,仍有其他机制兜底。
内网地址段的完整黑名单手动校验时必须覆盖所有可能的内网地址段。包括 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 这三个 RFC 1918 定义的私有地址段。还要包括 127.0.0.0/8 本地回环地址、169.254.0.0/16 链路本地地址、0.0.0.0/0 在某些系统中的特殊含义、以及 IPv6 的 ::1 回环地址和 fe80::/10 链路本地地址。云环境还需要额外屏蔽 169.254.169.254 这个 AWS、阿里云、腾讯云等云平台通用的元数据地址。建议使用成熟的 IP 地址库进行判断,而不是手写正则。
代码审计中的快速定位在代码审计中,快速定位此类风险的方法是搜索 HttpURLConnection 的实例化代码,检查是否调用了 setInstanceFollowRedirects(false)。同时搜索 URL.openConnection 的调用点,追溯传入的 URL 参数是否来自用户输入。对于 OkHtp 和 Apache HttpClents,搜索其 Builder 或配置代码,检查 followRedirects 相关设置。重点关注那些接收用户 URL、从数据库或消息队列中读取 URL 并发起请求的代码路径agram,这些是最容易出现 SSRF 的入口点。
实际案例中的绕过技巧真实攻击中,攻击者会利用短网址服务、开放重定向漏洞点等间接方式,将恶意地址隐藏在看似正常的 URL 后面。例如某些网站的开放重定向功能允许跳转到任意地址,攻击者将这类 URL 提交给目标系统,目标系统请求后经过多次跳转最终到达内网服务。还有利用 URL 解析差异的绕过,比如 http://evil.com@127.0.0.1/ 这种格式在不同库中的解析结果可能不同,有的认为主机是 evil.com,有的认为是 127.0.0.1。这些边缘情况都需要在安全校验中考虑。
日志与监控的配合即使做了完善的防御,也需要记录所有外发 HTTP 请求的日志,包括原始 URL、重定向链、最终请求地址。当检测到请求目标为内网地址时,除了阻断请求,还要触发告警,以便安全团队及时发现攻击行为。可以使用 ELK 或 Spluk 等日志分析平台,对请求目标 IP 进行实时分析,发现异常访问模式。对于云环境,还可以通过实例元数据服务的访问日志进行监控,正常情况下业务服务不应该访问元数据接口。
不同 JDK 版本的差异注意JDK 8、11、17 在 HttpURLConnection 的重定向实现上存在细微差异。早期版本对跨协议重定向的限制较为宽松,JDK 11 之后加强了安全检查。但即使在最新版本中,同协议重定向仍然是默认开启的,内网 SSRF 风险依然存在。因此不能依赖升级 JDK 版本来解决此问题,必须在代码层面做控制。另外,GraalVM 原生镜像中 HttpURLConnection 的行为可能与 HotSpt 有差异,使用前需要测试确认。
将用户输入的 URL 传递给 HTTP 客户端时,默认跟随重定向的行为等于把请求的控制权交给了外部服务端。关闭自动重定向,手动验证每一次跳转,是防范 SSRF 不可省略的一步。配合协议白名单、IP 黑名单、网路隔离,才能将风险控制在可接受范围内。
