跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)并不只是“让服务器代替用户发起一次请求”。在真实系统中,它往往与网络分区、云环境元数据服务、内部管理接口以及权限模型相互叠加,最终形成从外部输入到内部资源的访问链路。对这类问题进行审计时,单纯搜索 requests.get() 或 HttpClient 并不够,更重要的是理解数据流、请求边界和部署环境。

一、问题背景与常见场景

SSRF 通常出现在以下功能中:

  • 根据用户提交的 URL 抓取远程图片、网页或 RSS。
  • Webhook、回调地址、第三方通知地址配置。
  • 在线预览、文档转换、PDF 生成和远程资源解析。
  • 服务端健康检查、连通性测试和网络探测功能。
  • 代理下载、跳转跟随和文件导入功能。

这些功能本身并不一定有问题。风险主要取决于三个因素:用户是否能够影响目标地址,服务端是否会访问不应暴露的网络区域,以及返回内容是否会被直接回显或进一步处理。

审计时应把 SSRF 看作一条完整的数据流,而不是某一行危险函数。输入校验、DNS 解析、连接建立、重定向、响应读取和后续解析,每一个环节都可能改变最终风险。

二、先画出请求链路,再判断风险

一个典型的远程资源获取流程可以抽象为:

用户输入 URL
    ↓
参数校验与规范化
    ↓
DNS 解析
    ↓
建立 TCP/TLS 连接
    ↓
处理重定向
    ↓
读取响应
    ↓
解析或存储内容

代码审计时建议沿着这条链路逐项确认:

  • 输入是否允许完整 URL,还是只允许固定域名下的路径。
  • 校验发生在 DNS 解析之前还是之后。
  • 校验通过后是否重新解析 DNS。
  • HTTP 客户端是否默认跟随重定向。
  • 是否允许非 HTTP、HTTPS 协议。
  • 是否限制响应大小、连接超时和读取超时。
  • 响应是否会被当作 XML、图片、压缩包或模板继续处理。

如果只在入口处做字符串判断,例如判断 URL 是否包含某个域名,通常不足以证明安全。URL 解析存在用户名、端口、编码、IPv6、重定向和 DNS 变化等多个边界条件,字符串匹配很容易与实际连接目标不一致。

三、一个常见但不可靠的实现

下面的示例仅用于说明审计思路。它试图通过字符串判断阻止访问内部地址,但实际防护能力很弱:

public byte[] fetch(String input) throws IOException {
    if (input.contains("127.0.0.1") || input.contains("localhost")) {
        throw new IllegalArgumentException("blocked");
    }

    URL url = new URL(input);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setConnectTimeout(3000);
    conn.setReadTimeout(5000);
    return conn.getInputStream().readAllBytes();
}

这段代码至少存在以下问题:

  • 通过 IP 的其他表达形式可能绕过文本判断。
  • DNS 解析结果没有进行地址分类。
  • 没有限制重定向,目标服务器可能返回另一个地址。
  • 未限制响应体大小,容易造成内存或磁盘资源耗尽。
  • 允许任意协议转换或 URL 解析行为,具体风险取决于客户端实现。
  • 没有明确限制端口和出口网络范围。

四、正确的审计方法:验证“最终连接目标”

防护的核心不是判断用户输入长什么样,而是确认客户端最终连接到什么地址。建议把目标解析为结构化对象,并对主机、端口、协议和解析结果分别检查。

在 Java 服务中,可以将校验逻辑拆成以下几个阶段:

URI uri = URI.create(input);
String scheme = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();

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

if (host == null || host.isBlank()) {
    throw new IllegalArgumentException("invalid host");
}

if (port != -1 && port != 80 && port != 443) {
    throw new IllegalArgumentException("unsupported port");
}

这只是基础校验,不能单独作为完整防护。之后还需要解析主机名,并对每个解析结果进行地址分类。对于业务不需要访问内网资源的服务,通常应拒绝回环地址、链路本地地址、私有地址、未指定地址以及其他保留地址。

InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
    if (address.isAnyLocalAddress()
            || address.isLoopbackAddress()
            || address.isLinkLocalAddress()
            || address.isSiteLocalAddress()) {
        throw new IllegalArgumentException("destination is not allowed");
    }
}

实际项目中不建议仅依赖这几个便捷方法。应结合组织的网络策略,对 IPv4、IPv6、云平台地址段、办公网地址段和容器网络地址段进行统一封装,并为该组件配套单元测试和回归测试。

五、DNS 重绑定与校验时机问题

一个经常被忽略的问题是:校验阶段解析到的地址,不一定是连接阶段使用的地址。若应用先解析域名并判断结果安全,随后由 HTTP 客户端再次解析域名,就存在两次解析结果不一致的可能。

因此,较稳妥的做法包括:

  • 在受控网络环境中使用固定的出口代理,由代理统一执行最终连接和地址策略。
  • 使用能够固定解析结果或提供自定义 DNS 解析器的 HTTP 客户端。
  • 在建立连接前后都执行地址检查,并避免客户端自动重新解析未经校验的主机名。
  • 通过网络层策略阻断应用容器访问不必要的内网网段,不能只依赖业务代码。

在审计报告中,建议明确写出“校验点”和“连接点”是否一致,而不是笼统描述为“存在 DNS 绕过”。这样更便于开发人员定位客户端配置和网络出口问题。

六、重定向是第二个高频风险点

即使初始 URL 通过了校验,服务端也可能返回 3xx 响应,引导客户端访问另一个地址。如果 HTTP 客户端默认跟随重定向,那么真正的访问目标已经发生变化。

安全处理方式有两种:

  • 业务不需要重定向时,直接关闭自动跟随,并将 3xx 作为失败处理。
  • 业务确实需要重定向时,对每一跳的目标重新解析、重新校验,并限制最大跳转次数。
HttpClient client = HttpClient.newBuilder()
        .followRedirects(HttpClient.Redirect.NEVER)
        .connectTimeout(Duration.ofSeconds(3))
        .build();

对于下载器、图片代理等功能,建议把重定向次数限制在较小范围内,并限制每次跳转只能使用允许的协议和端口。不要仅校验初始地址。

七、响应内容同样属于攻击面

SSRF 的影响不只来自“能否访问某个地址”,还取决于应用如何处理响应。常见的二次风险包括:

  • 读取内部接口返回的配置、调试信息或身份凭据。
  • 将响应内容直接回显给前端,形成信息泄露。
  • 把响应当作 XML 解析,引入 XXE 风险。
  • 把响应当作图片、压缩包或文档处理,触发解析器漏洞。
  • 无限读取大响应,导致内存、磁盘或线程资源耗尽。

因此,远程资源获取功能至少应设置连接超时、读取超时、最大响应大小、最大重定向次数和允许的 Content-Type。不要只根据响应头判断安全,服务端返回的头部本身也可能不可信,必要时应对实际内容做有限度的格式识别。

long maxBytes = 5 * 1024 * 1024;
try (InputStream in = response.body()) {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] buffer = new byte[8192];
    long total = 0;
    int n;
    while ((n = in.read(buffer)) != -1) {
        total += n;
        if (total > maxBytes) {
            throw new IOException("response too large");
        }
        out.write(buffer, 0, n);
    }
}

八、在隔离环境中进行安全验证

验证 SSRF 时不应直接探测生产内网,也不应访问真实的管理接口或云平台元数据服务。更推荐建立最小化实验环境:

  • 使用本地容器启动一个业务服务和一个仅用于记录请求的测试服务。
  • 让测试服务返回固定的非敏感标记,例如 TEST-SSRF-OK。
  • 通过应用日志、容器网络日志和代理日志确认实际请求目标。
  • 对重定向、超时、超大响应和不同地址类型分别编写自动化测试。

例如,可以在本地测试服务中记录请求来源、Host、路径和 User-Agent,再由业务服务访问该测试地址。测试的重点是确认请求是否按照预期被限制,而不是尝试读取任何真实敏感信息。

curl --max-time 3  SSRF,合理的身份权限也能降低进一步影响。

十、代码审计中的排查清单

  • 搜索所有由用户输入决定目标地址的代码路径,而不只是搜索 HTTP 客户端调用。
  • 检查 URL 是否经过结构化解析,避免纯字符串黑名单。
  • 确认是否校验协议、主机、端口和解析后的全部地址。
  • 检查 IPv4、IPv6、域名多解析结果以及重定向处理。
  • 确认 DNS 校验与实际连接是否使用同一套解析结果。
  • 检查连接、读取、响应大小和重定向次数限制。
  • 确认响应是否进入 XML、图片、压缩包、模板或脚本解析流程。
  • 检查日志中是否包含完整 URL、Authorization 头或敏感响应内容。
  • 结合容器、主机、防火墙和云网络策略判断真实可达范围。
  • 为修复方案补充回归测试,避免后续重构重新引入问题。

十一、修复后的回归测试设计

回归测试不应只验证一个被拦截的地址,而应覆盖整个请求状态机。可以至少准备以下测试类别:

  • 合法的业务域名和合法端口能够正常访问。
  • 回环、私有、链路本地和未指定地址被拒绝。
  • 主机名解析得到多个地址时,只要存在不允许地址就拒绝。
  • 初始地址合法、重定向目标不合法时,请求被终止。
  • 响应超过大小限制时,连接被安全关闭。
  • 连接超时和读取超时不会长期占用工作线程。
  • 不支持的协议和端口无法进入底层客户端。
  • 错误日志包含足够的排查信息,但不泄露敏感响应内容。

总结

SSRF 审计的关键不是寻找某个“万能黑名单”,而是建立清晰的请求边界:谁提供目标地址,应用解析出了什么,最终连接到了哪里,响应又被如何处理。比较可靠的治理方式通常由四部分组成:结构化解析与严格校验、对 DNS 和重定向保持一致控制、限制响应处理资源,以及通过代理和网络隔离补充应用层防护。

从工程实践看,最值得优先改进的往往不是某一行过滤代码,而是把远程请求能力集中到一个经过审计的组件中,统一处理协议、地址、端口、超时、大小和日志策略。这样既能降低修复成本,也能避免不同业务各自实现一套不完整的 SSRF 防护逻辑。

服务器与云环境安全示意图
服务器、云资产与权限边界梳理示意

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.