跳转到帖子

SSRF(Server-Side Request Forgery,服务端请求伪造)并不是“发现一个可以访问 URL 的参数”这么简单。真正有价值的审计,需要回答三个问题:请求是否由服务端发起、攻击者能控制请求的哪些部分、服务端出站网络能够到达什么位置。

在实际项目中,SSRF 常出现在图片抓取、Webhook、在线预览、文件导入、PDF 转换、第三方接口代理等功能里。它的风险也取决于网络边界和权限配置:一个只能访问公网的请求代理,影响可能有限;如果能够触达内部管理接口、云环境元数据服务或未鉴权的运维组件,风险就会明显扩大。

一、审计场景与基本判断

常见入口包括以下几类:

  • 根据 URL 下载图片、头像或远程附件。
  • 将 Markdown、HTML 或文档转换为 PDF。
  • 向用户指定的 Webhook 地址发送通知。
  • 通过 URL 抓取网页标题、生成缩略图或进行内容预览。
  • 后台提供的“连通性检测”“接口测试”“导入远程配置”等功能。

审计时不要只搜索参数名 url。更可靠的方法是从网络请求 API 反向追踪数据流,重点关注 Java 的 HttpClient、URLConnection、OkHttp,Go 的 http.Client,Python 的 requests、urllib,以及 Node.js 中的 fetch、axios 等调用点。

判断 SSRF 的核心不是“是否使用了 HTTP 客户端”,而是“外部可控数据是否能影响请求目标,并且缺少足够的目标校验和出站限制”。

二、从数据流定位真正的请求点

以一个简化的 Java 服务为例,控制器接收用户提交的 URL,随后交给业务层下载内容:

@PostMapping("/preview")
public PreviewResult preview(@RequestParam String target) {
    return previewService.fetch(target);
}

public byte[] fetch(String target) {
    try {
        URI uri = URI.create(target);
        HttpRequest request = HttpRequest.newBuilder(uri)
                .timeout(Duration.ofSeconds(5))
                .GET()
                .build();
        return httpClient.send(request,
                HttpResponse.BodyHandlers.ofByteArray()).body();
    } catch (Exception e) {
        throw new IllegalArgumentException("fetch failed", e);
    }
}

这个例子中,风险点至少有四个:

  • target 完全来自请求参数。
  • 没有限制协议,URI 解析结果可能包含非预期 scheme。
  • 没有校验目标 IP 是否属于内网、回环、链路本地或保留地址。
  • 没有说明重定向策略、响应大小、响应类型和连接数量。

实际审计时还要继续追踪 httpClient 的构造位置。即使业务代码没有显式处理重定向,底层客户端也可能默认跟随;即使第一轮校验通过,重定向后的第二个地址仍可能落入禁止网段。

三、URL 校验中最容易遗漏的边界

1. 协议与端口

一般只允许业务确实需要的协议,例如 http 和 https。不要把协议校验建立在字符串前缀上,例如判断是否以 开头。应当使用标准 URI 解析器读取 scheme、host、port,并拒绝空主机、异常端口和不必要的协议。

2. 主机名解析与 IP 校验

仅检查用户输入中是否包含 127.0.0.1、localhost 等字符串是不够的。攻击者可以使用域名、IPv6、不同进制表示或 DNS 解析结果变化来绕过简单规则。

较稳妥的流程是:

  1. 先解析 URI,确认协议、主机名和端口符合业务要求。
  2. 对主机名进行 DNS 解析。
  3. 检查所有解析结果,而不是只检查第一个地址。
  4. 拒绝回环、私有、链路本地、组播、未分配和其他保留地址。
  5. 建立连接前再次确认目标地址,避免校验与连接之间存在时间窗口。

在 Java 中可以将地址分类判断封装成独立组件,避免散落在多个控制器里:

boolean isBlocked(InetAddress address) {
    return address.isAnyLocalAddress()
        || address.isLoopbackAddress()
        || address.isLinkLocalAddress()
        || address.isSiteLocalAddress()
        || isReservedRange(address);
}

isSiteLocalAddress() 不能覆盖所有需要禁止的范围,生产环境应根据 IPv4、IPv6 和云网络规划补充明确的 CIDR 列表,并通过单元测试覆盖边界地址。

3. DNS 重绑定问题

如果校验阶段解析到公网地址,而真正建立连接时再次解析得到内部地址,就可能出现 DNS 重绑定。应尽量让校验结果与实际连接使用同一个解析结果,或者在受控网络代理中统一完成解析和连接。对于高风险场景,建议不允许服务端直接解析用户提供的任意域名,而是采用固定出口代理和域名白名单。

4. 重定向

重定向是审计中经常被忽略的一层。第一跳地址合法,不代表 Location 指向的下一跳也合法。修复方案通常有两种:

  • 业务不需要重定向时直接关闭自动跟随。
  • 必须支持重定向时,每一跳都重新执行协议、域名、解析结果和端口校验,并限制最大跳数。

四、构造安全的请求流程

一个相对完整的安全请求流程应当同时控制目标、协议、连接和响应。下面是逻辑示例,重点在流程而非直接复制到生产环境:

URI uri = parseAndValidate(input);

if (!Set.of("http", "https").contains(uri.getScheme().toLowerCase())) {
    throw new SecurityException("unsupported scheme");
}

List addresses = resolver.resolve(uri.getHost());
if (addresses.isEmpty() || addresses.stream().anyMatch(this::isBlocked)) {
    throw new SecurityException("blocked destination");
}

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

HttpRequest request = HttpRequest.newBuilder(uri)
        .timeout(Duration.ofSeconds(5))
        .header("Accept", "image/*")
        .GET()
        .build();

HttpResponse<InputStream> response = client.send(
        request, HttpResponse.BodyHandlers.ofInputStream());

if (response.statusCode() / 100 == 3) {
    throw new SecurityException("redirect is not allowed");
}
if (!isAllowedContentType(response.headers().firstValue("Content-Type"))) {
    throw new SecurityException("unexpected content type");
}
byte[] body = readAtMost(response.body(), 5 * 1024 * 1024);

这里仍然需要注意连接实际使用的地址是否与校验地址一致。不同 HTTP 客户端的 DNS、连接池、代理和 IPv6 行为可能不同,不能只看业务层代码就认为校验已经生效。

五、通过网络层降低 SSRF 影响

应用层校验不是唯一防线。更可靠的设计是让应用运行在默认拒绝出站访问的网络环境中,只开放业务必需的目标。

  • 在容器、主机防火墙或云安全组中限制出站网段和端口。
  • 将远程抓取功能放入隔离的任务服务,不与核心业务共享高权限网络身份。
  • 通过固定的出站代理访问公网,并在代理层做域名、端口和响应大小控制。
  • 避免让业务实例直接接触云平台元数据服务;在云环境中使用平台提供的元数据访问保护机制。
  • 对 DNS 请求、HTTP 出站、异常端口连接建立审计日志和告警。

需要注意的是,网络隔离不能替代应用层修复。应用仍应限制 URL、重定向、响应体和请求时间,因为 SSRF 还可能被用于访问内部但本身不属于高敏感网段的服务,或者消耗连接和带宽形成拒绝服务。

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

验证应在本地测试环境或明确授权的测试网络中进行,不要直接探测生产内网。可以搭建两个本地服务:

  • 服务 A 模拟业务接口,接收并处理用户提交的 URL。
  • 服务 B 模拟允许访问的资源,同时记录请求头、路径和重定向行为。

首先验证正常业务:

curl -X POST  \
  --data-urlencode 'target= Content-Type 等情况,确认应用能够按预期拒绝,并且不会出现长时间占用线程、内存持续增长或连接池耗尽。

建议将以下测试纳入自动化用例:

测试类别验证内容预期结果
协议非 HTTP/HTTPS scheme拒绝请求
地址回环、私有、链路本地和 IPv6 保留地址拒绝请求
解析一个域名返回多个地址所有地址均通过检查,否则拒绝
重定向合法地址跳转到禁止地址拒绝或不跟随
响应超大内容、错误类型、慢速传输受大小、类型和时间限制
异常DNS 失败、连接超时、证书错误安全失败且日志可追踪

七、日志与告警应该记录什么

SSRF 防护失败后,如果日志只记录“fetch failed”,后续很难定位问题。建议记录结构化字段:

  • 业务功能和请求 trace ID。
  • 经过规范化处理后的 scheme、域名和端口。
  • 解析得到的地址类别,但避免记录不必要的敏感响应内容。
  • 拒绝原因,例如协议不允许、目标地址被禁止、重定向超限或响应过大。
  • 连接耗时、响应状态码和响应大小。

日志中的用户输入应进行长度限制和转义,避免攻击者利用 URL 或错误信息制造日志注入。对于短时间内大量访问不同域名、频繁命中禁止地址或持续触发超时的行为,可以结合账号、租户和来源网络进行限速和告警。

八、代码审计中的常见误区

  • 只做字符串黑名单:黑名单很容易遗漏等价表示、IPv6 和 DNS 场景,应优先采用解析后的地址分类与白名单策略。
  • 只校验第一次请求:重定向、代理和连接池可能让后续连接落到不同目标,必须覆盖完整请求链路。
  • 只检查域名后缀:例如允许某个后缀并不能自动证明其解析地址安全,也要防止相似域名和解析漂移。
  • 只关注数据泄露:SSRF 同样可能造成内网服务探测、异常请求触发和资源消耗。
  • 把超时当成完整防护:超时只能降低部分资源消耗,不能替代目标校验、网络隔离和响应大小限制。

总结

SSRF 审计的重点是围绕“输入—解析—连接—重定向—响应—网络出口”建立完整的数据流视图。单独增加一个 URL 黑名单,通常只能挡住少量明显样本,无法应对解析差异、重定向、DNS 变化和多协议客户端行为。

更可执行的修复组合是:只允许业务需要的协议和端口;对解析后的所有地址执行统一的网段校验;谨慎处理 DNS 和重定向;限制连接时间、响应大小和内容类型;在网络层实施默认拒绝的出站策略;最后用本地授权环境完成回归测试和日志验证。这样才能把 SSRF 从一个模糊的风险点,落实为可审计、可测试、可持续维护的安全控制。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.