跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)并不只存在于一个简单的“传入 URL、后端发起请求”接口中。在实际项目里,图片抓取、Webhook 调试、文档预览、远程文件导入、健康检查等功能,都可能形成出站请求能力。真正困难的地方通常不是发现请求,而是判断请求是否能够到达不应访问的网络边界,以及应用是否在重定向、DNS 解析和代理配置下仍然保持安全。

本文以代码审计和内部复现为主线,整理一套不依赖攻击内网的 SSRF 分析方法。示例均应在本地靶场或经过授权的测试环境中进行,重点放在风险确认、边界控制和修复验证。

一、问题场景与审计目标

典型业务代码大致如下:

String target = request.getParameter('url');
URL url = new URL(target);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.setReadTimeout(5000);
return readBody(connection.getInputStream());

这段代码的问题不在于使用了某个特定 HTTP 类,而在于它把用户输入直接交给了 URL 解析器和网络栈。审计时需要回答四个问题:

  • 请求是否允许访问回环地址、私有地址、链路本地地址或本机 Unix Socket 代理?
  • 域名解析发生在哪里,解析结果是否经过校验?
  • 服务端是否自动跟随重定向,重定向后的目标是否重新校验?
  • 应用运行环境是否有网络层面的出口限制,代理和 DNS 配置是否会改变实际访问路径?

如果只能证明“服务器发出了请求”,还不能直接判定为高风险 SSRF。应结合可达性、响应可见性、凭据暴露、网络隔离和业务权限进行分级。

二、为什么常见黑名单容易失效

很多修复方案只拦截字符串中的 127.0.0.1、localhost 或 10.0.0.0/8。这种方式存在几个明显缺陷。

  • 域名可能解析到私有地址,但字符串本身看起来是公网域名。
  • 同一个域名可能在不同解析时刻返回不同地址,导致校验和实际连接结果不一致。
  • IPv4 和 IPv6 的表示形式较多,字符串匹配很容易遗漏边界情况。
  • HTTP 客户端可能自动跟随重定向,初始 URL 合法并不意味着最终目标合法。
  • 环境变量、系统代理或自定义 DNS 可能使请求不经过开发者预期的网络路径。

因此,安全校验应建立在规范化后的 URL、解析后的 IP 和最终连接目标之上,而不是只对原始字符串做正则匹配。

三、推荐的审计流程

1. 先画出请求链路

不要一开始就陷入某个 URL 解析函数。先梳理完整链路:输入来源、参数解码、URL 拼接、解析、DNS、代理、重定向、响应读取和日志记录。特别关注以下代码模式:

new URL(userInput)
URI.create(userInput)
HttpClient.send(request, handler)
requests.get(userInput)
axios.get(userInput)
urllib.request.urlopen(userInput)
WebClient.create(userInput)

除了直接传入 URL,还要检查字符串拼接、Base URL 配置、模板变量和 JSON 中的嵌套字段。例如“域名由服务端固定、路径由用户控制”的设计,也可能因为路径解析、反向代理或协议处理不当形成越权访问。

2. 明确允许的业务协议

如果业务只需要抓取公开 HTTPS 资源,应只允许 https,并拒绝 file、ftp、gopher、jar 等不必要协议。协议限制应在解析后判断,而不是只检查字符串前缀。

URI uri = URI.create(input);
String scheme = uri.getScheme();
if (scheme == null || !scheme.equalsIgnoreCase('https')) {
    throw new IllegalArgumentException('unsupported scheme');
}

这里的协议白名单只是第一层控制,不能替代地址校验。对于需要支持 HTTP 的内部场景,应通过明确的业务白名单实现,而不是对所有目标开放。

3. 规范化主机名并执行 DNS 校验

主机名校验建议遵循“解析、分类、连接前再次确认”的思路。解析结果中的每一个地址都应进行网络范围判断,不能只检查第一个结果。

InetAddress[] addresses = InetAddress.getAllByName(host);
for (InetAddress address : addresses) {
    if (address.isAnyLocalAddress()
        || address.isLoopbackAddress()
        || address.isLinkLocalAddress()
        || address.isSiteLocalAddress()) {
        throw new SecurityException('blocked destination');
    }
}

不同语言和运行时对私有地址、保留地址、IPv6 范围的判断能力不完全一致。生产实现中应结合成熟的 IP 地址库,明确覆盖 RFC 1918 私网、回环地址、链路本地地址、未指定地址、组播地址、IPv6 本地地址以及企业自定义内网网段。

仅在业务代码中解析一次 DNS 仍然可能存在时间窗口:校验阶段和连接阶段可能得到不同结果。更稳妥的做法是使用受控解析器、固定出口代理或网络层 egress 策略,并避免让客户端自行按照不受控的系统配置解析和连接。

4. 关闭自动重定向,或对每一跳重新校验

自动重定向是审计中经常遗漏的路径。初始地址通过校验后,服务端可能被 301、302、307 或 308 引导到另一个主机。建议默认关闭自动重定向;确需支持时,限制跳转次数,并对 Location 解析后的协议、主机、端口和 IP 重新执行完整校验。

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.NEVER)
    .connectTimeout(Duration.ofSeconds(3))
    .build();

重定向校验不能只比较域名字符串。应考虑相对 URL、端口变化、大小写、尾部点号、国际化域名和解析结果变化。对于文件导入类功能,通常更推荐由服务端维护目标站点白名单,从根源上减少任意跳转的必要性。

5. 限制端口、响应大小和请求时间

SSRF 风险不仅取决于目标地址,也取决于请求能力。应结合业务需求限制端口范围、连接超时、读取超时、最大响应体和最大重定向次数。对于下载类接口,还应限制压缩解压后的大小,避免通过压缩数据造成资源耗尽。

  • 协议:仅开放业务需要的 HTTPS 或有限 HTTP。
  • 端口:优先只允许 443,必要时显式配置端口白名单。
  • 连接超时:通常设置为数秒级,避免线程长期占用。
  • 读取超时:避免慢速响应拖垮连接池。
  • 响应大小:在读取过程中累计字节数,超过上限立即中止。
  • 并发量:使用独立连接池和限流,避免出站接口成为资源消耗点。

四、一个相对完整的安全实现思路

下面的伪代码展示校验顺序。它不是可直接复制到所有项目的通用组件,实际使用时还需要处理国际化域名、代理策略、IPv6、日志脱敏和连接库的具体行为。

URI uri = parseAndNormalize(input);

requireScheme(uri, Set.of('https'));
requireHost(uri);
requireAllowedPort(uri, Set.of(443));

String host = normalizeHostname(uri.getHost());
List<InetAddress> addresses = resolver.resolveAll(host);
for (InetAddress address : addresses) {
    if (networkPolicy.isBlocked(address)) {
        throw new SecurityException('destination is not allowed');
    }
}

HttpResponse response = client.sendWithoutRedirect(uri);
if (response.isRedirect()) {
    URI next = resolve(uri, response.location());
    validateAgain(next);
    response = client.sendWithoutRedirect(next);
}

return readAtMost(response.body(), MAX_BYTES);

实际工程中,最好把 URL 校验、DNS 解析、连接和响应读取封装成一个受控组件,避免每个业务团队各自实现一套不完整的逻辑。安全组件应提供明确的默认值,并把拒绝原因记录为结构化事件,便于后续审计。

五、如何在授权环境中验证漏洞

验证 SSRF 不应以访问真实内网服务为前提。可以在本地搭建两个容器:一个运行待测应用,另一个运行仅用于测试的 HTTP 服务。测试服务记录请求方法、Host、路径和来源地址,但不提供任何敏感内容。通过日志即可确认请求是否从服务端发出、是否携带了不应传递的请求头,以及重定向是否被跟随。

docker network create ssrf-lab
# 在隔离实验网络中启动待测应用和回显服务
# 只使用测试数据,不连接公司内网、云平台元数据服务或真实业务系统

验证项目至少包括:

  • 合法公网 HTTPS 地址是否正常工作。
  • 回环、私有、链路本地和未指定地址是否被拒绝。
  • 域名解析到受限地址时是否被拒绝。
  • 初始地址合法但重定向到受限地址时是否被拒绝。
  • 超大响应、慢速响应和多次重定向是否受到限制。
  • IPv4 与 IPv6 场景下策略是否一致。

测试日志中不要记录完整的 Authorization、Cookie、签名参数或下载内容。对 URL 中的凭据和敏感查询参数进行脱敏,避免“为了验证漏洞”反而产生新的敏感信息泄露。

六、网络层控制比应用层黑名单更可靠

应用层校验是必要条件,但不应成为唯一防线。生产环境建议同时配置网络出口策略:

  • 将需要出网的服务放入独立网络区域,默认拒绝访问内部管理网段。
  • 通过固定的出站代理统一执行域名、地址、端口和审计策略。
  • 限制容器或虚拟机访问云平台元数据地址及管理平面地址。
  • 对 DNS 使用受控解析服务,避免应用直接使用不可信的递归解析路径。
  • 为出站请求建立独立的监控指标,包括目标类别、失败原因、响应大小和耗时。

网络层策略还可以降低代码遗漏的影响。例如某个业务组件忘记校验重定向,出口防火墙仍可阻断其访问内部网段。反过来,网络层阻断也无法替代应用层的协议、端口和业务白名单控制,两者应当叠加使用。

七、常见误区与修复注意点

  • 只拦截 localhost:无法覆盖私网地址、IPv6 回环、链路本地地址和解析后的内网目标。
  • 只校验首次解析:连接时可能重新解析,或被代理、重定向改变实际目的地。
  • 只检查响应状态码:即使响应最终不可见,请求本身仍可能触发内部操作或产生副作用。
  • 允许任意协议:不同协议的解析和连接语义差异很大,业务不需要的协议应直接关闭。
  • 把安全校验放在日志之后:日志系统可能先解析或访问 URL,审计路径也要纳入检查范围。
  • 修复后只测一个地址:应覆盖解析、重定向、代理、IPv4/IPv6 和资源限制等不同路径。

八、风险分级建议

风险评估应结合实际能力,而不是看到“可发起请求”就直接套用最高等级。可以从以下维度综合判断:

维度低风险特征高风险特征
目标范围固定公网白名单任意主机、内部网段或管理地址
协议能力仅 HTTPS支持多种非业务协议
响应能力只返回固定状态可读取并回显完整响应
网络位置受限出网环境可访问生产内网或敏感管理平面
凭据暴露无高权限身份运行身份可访问敏感服务

在漏洞报告中应说明复现条件、可达范围、响应可见性、环境限制和修复建议,避免使用未经授权的真实目标作为证明材料。

总结

SSRF 审计的核心不是寻找一个可以传入 URL 的参数,而是确认整个出站请求链路是否具备越过网络边界的能力。可靠的处理方式通常包括:协议白名单、规范化解析、解析结果校验、限制端口、关闭或严格校验重定向、限制响应和连接资源,并辅以固定出口、网络隔离和受控 DNS。

对于代码审计人员,建议把“DNS 解析在哪里发生、重定向由谁处理、最终连接到哪里”作为固定检查项。对于开发团队,最好将这些逻辑封装成统一组件,并用本地隔离环境覆盖正常请求、受限地址、重定向和资源耗尽等测试场景。这样既能减少误报,也能避免只修复一个字符串匹配点而留下完整的请求链路风险。

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

0篇意见

推荐意见

没有意见。

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