跳转到帖子

服务端请求伪造(SSRF)并不只是“接口能不能访问内网”的问题。实际审计中,漏洞往往出现在 URL 解析、DNS 解析、重定向处理和出站网络策略之间的不一致:应用认为目标是公网域名,底层网络却最终连接到了本机、内网服务或云环境管理接口。

这类问题的修复也不能依赖一个简单的字符串黑名单。本文以代码审计和安全验证为主线,整理 SSRF 的常见成因、审计路径、修复思路和测试重点。示例均应在本地靶场或授权测试环境中使用,不涉及访问真实内部系统。

一、典型业务场景

以下功能最容易引入 SSRF:

  • 通过 URL 抓取图片、文档或网页内容;
  • Webhook、回调地址和第三方通知配置;
  • 远程导入、在线预览、网页截图和 PDF 转换;
  • 根据用户提供的地址检查站点状态;
  • 解析 RSS、OpenAPI 文档或远程配置文件。

一个常见的危险实现如下:

String target = request.getParameter("url");
URL url = new URL(target);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(3000);
conn.setReadTimeout(5000);
return conn.getInputStream().readAllBytes();

这里没有限制协议、目标地址、端口、响应大小和重定向行为。即使增加了 startsWith("http") 之类的检查,也不能解决核心问题,因为 URL 的语义必须经过规范化解析,不能用字符串匹配代替。

二、审计时先确认“请求链”

审计不要只盯着控制器。应沿着用户输入追踪到真正建立网络连接的位置,重点确认以下问题:

  • 输入是否进入了 HTTP 客户端、DNS 客户端、文件读取 API 或第三方抓取组件;
  • 是否存在二次解析,例如先把 URL 交给代理,再由代理重新解析域名;
  • 是否启用了自动重定向;
  • 域名解析发生在应用、代理还是容器网络层;
  • 响应是否会被完整读入内存、落盘或再次交给解析器;
  • 失败信息、响应头和响应体是否会回显给用户。

在 Java 项目中,可以重点搜索 URL、URI、HttpClient、HttpURLConnection、RestTemplate、WebClient、OkHttp 以及自定义的下载工具类。Python 项目则可关注 requests、urllib、aiohttp 和基于浏览器的抓取组件。

三、最容易被忽略的解析问题

1. 协议限制不完整

业务通常只需要 HTTP 和 HTTPS,却可能意外支持 file、ftp、gopher 或其他由底层库处理的协议。修复时应采用正向允许列表,而不是维护不断增长的危险协议黑名单。

if (!scheme.equals("http") && !scheme.equals("https")) {
    throw new IllegalArgumentException("unsupported scheme");
}

2. 主机名和端口判断不一致

字符串中出现一个允许域名,并不代表最终连接目标就是该域名。审计时要关注用户名信息、端口、IPv6 表示、尾随点、大小写和编码字符等情况。主机名应使用标准 URI 解析器取得,不能通过截取字符串判断域名后缀。

例如,判断是否属于 example.com 时,不能只检查是否以该字符串结尾。正确的边界应是主机名等于该域名,或以 .example.com 作为完整后缀,否则类似 example.com.attacker.test 的主机名可能被错误放行。

3. DNS 解析和实际连接之间的竞态

一种常见的修复方式是先解析域名,确认返回地址不是内网地址,再交给 HTTP 客户端重新连接。问题在于,校验和连接可能使用了两次不同的 DNS 结果,形成检查时与使用时不一致。

更稳妥的设计是让出站代理或网络策略完成最终连接控制,应用侧只负责业务级允许列表。若必须在应用中实现,应尽量复用已校验的解析结果,禁用自动代理和不受控的重解析,并对每次重定向重新执行完整校验。

4. 重定向绕过初始校验

即使首次 URL 是公开站点,也可能返回指向内网地址的 30x 响应。因此 SSRF 防护不能只检查首次请求。建议默认关闭自动重定向;确需支持时,应限制跳转次数,并对每个新地址重新检查协议、主机、端口和解析结果。

四、推荐的防护分层

第一层:业务允许列表

如果业务目标固定,例如只允许访问几个合作方,应保存经过审核的域名或资源标识,避免让用户直接提交任意 URL。域名、端口、路径和证书校验都应纳入规则。

第二层:规范化后再校验

流程顺序建议如下:

  1. 解析 URI,拒绝缺少主机名、异常编码和不支持的协议;
  2. 规范化主机名和端口,明确默认端口的处理方式;
  3. 解析所有地址记录,拒绝回环、链路本地、私有、保留和未分配地址;
  4. 限制端口范围,除非业务确实需要,否则只允许 80 和 443;
  5. 关闭或严格控制重定向;
  6. 限制连接超时、读取超时、响应大小和总下载时间。

地址分类不能只判断 IPv4。生产环境还应覆盖 IPv6 的回环地址、链路本地地址、唯一本地地址以及 IPv4 映射地址。下面是防护逻辑的示意代码,具体实现应使用经过验证的网络地址库:

URI uri = URI.create(input);
String scheme = uri.getScheme();
if (!Set.of("http", "https").contains(scheme)) {
    throw new SecurityException("scheme denied");
}

String host = IDN.toASCII(uri.getHost()).toLowerCase(Locale.ROOT);
int port = uri.getPort() == -1 ? defaultPort(scheme) : uri.getPort();
if (!allowedPort(port) || host.isBlank()) {
    throw new SecurityException("host or port denied");
}

for (InetAddress address : resolver.resolve(host)) {
    if (isLoopback(address) || isPrivate(address)
        || isLinkLocal(address) || isReserved(address)) {
        throw new SecurityException("non-public address denied");
    }
}

这段代码只表达控制点,不应直接复制到生产环境。尤其是 isPrivate 和 isReserved 的实现,要确认依赖库对 IPv4、IPv6 和映射地址的覆盖范围。

第三层:网络出口控制

应用层校验容易受到 DNS、代理和组件行为差异影响。更可靠的做法是在容器、主机或出站代理层限制服务访问范围:

  • 抓取服务使用独立网络区域,默认拒绝访问内网网段;
  • 通过防火墙或安全组限制出站端口;
  • 禁止访问云平台管理地址和本机管理接口;
  • 使用专用 DNS 解析策略,避免应用直接访问不必要的内部解析服务;
  • 在代理层记录目标域名、解析地址、最终连接地址和重定向链。

五、如何在授权环境中验证修复

建议搭建两个本地服务:一个模拟业务允许访问的公网目标,另一个模拟不应被访问的内部服务。测试重点不是读取敏感内容,而是验证请求是否被策略拒绝,并检查日志是否记录了原因。

# 启动本地测试服务,仅用于本机靶场
python3 -m http.server 18080 --bind 127.0.0.1

测试用例至少应覆盖:

  • 正常的 HTTP 和 HTTPS 地址;
  • 不支持的协议;
  • 无主机名、非法端口和异常 URL 编码;
  • 解析到回环、私有、链路本地和 IPv6 本地地址的域名;
  • 返回重定向的地址;
  • 多次解析结果不同的域名;
  • 超大响应、慢响应和连接失败场景。

验证结果应同时观察应用日志、代理日志和网络设备日志。只看 HTTP 状态码不够,因为某些客户端在异常时仍可能建立了 TCP 连接。

六、日志与监控中的关键字段

出站请求建议记录请求来源、业务接口、原始 URL 的脱敏形式、规范化主机名、目标端口、解析到的地址、是否发生重定向、拒绝原因和耗时。不要把完整响应体或可能包含凭据的 URL 直接写入日志。

监控上可以关注以下异常:

  • 同一接口短时间访问大量不同域名;
  • 请求集中出现在非业务端口;
  • 大量 DNS 失败、连接超时或重定向;
  • 抓取服务访问地址空间异常分散;
  • 应用进程出现不符合业务的长连接和出站流量。

七、审计中的常见误区

  • 只做黑名单。 黑名单难以覆盖所有地址表示和协议变体,应该优先采用允许列表。
  • 只校验域名字符串。 必须关注解析后的地址以及最终连接目标。
  • 只校验第一次请求。 重定向、代理和二次解析都会改变目标。
  • 只依赖应用代码。 出站网络隔离是重要的第二道防线。
  • 为了方便开启自动跟随重定向。 这会让校验边界变得模糊,应默认关闭。
  • 忽略响应处理。 大响应、压缩炸弹和恶意内容解析同样可能造成拒绝服务或二次漏洞。

总结

SSRF 的本质是“用户可控输入影响了服务端的出站连接”,真正困难的部分在于 URL 解析、DNS、重定向、代理和网络层之间的边界。代码审计时,应从输入点追踪到实际连接点;修复时,应采用协议和目标的正向允许列表、对每次跳转重新校验,并通过出站网络策略限制应用能力。

如果业务允许,最有效的方案通常不是让主应用直接访问任意 URL,而是将抓取能力拆分为独立服务,配合严格的出口代理、资源限制和审计日志。这样即使某一层校验出现遗漏,也能把风险控制在可观测、可阻断的范围内。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…