跳转到帖子

服务端请求伪造(SSRF)并不只是一条危险的 URL 规则。它通常出现在“后端代替用户访问资源”的功能中,例如图片抓取、Webhook 回调、文档解析、URL 预览和远程健康检查。真正困难的地方在于:输入校验、URL 解析、DNS 解析、连接建立以及重定向处理,往往由不同组件完成,开发者看到的字符串和网络库最终访问的地址可能并不相同。

本文以代码审计和修复为主线,整理一套适用于日常项目的 SSRF 分析方法。示例只针对本地测试环境和自建模拟服务,不涉及访问真实内网资源。

一、先判断功能是否具备 SSRF 条件

审计时不要只搜索 http 或 URL。更有效的做法是先找出所有“服务端发起网络请求”的调用点,再向上追踪参数来源。

  • Java 常见调用点:HttpClient、URLConnection、OkHttp、Apache HttpClient。
  • Python 常见调用点:requests、urllib、httpx。
  • Go 常见调用点:http.Get、http.Client.Do、自定义 Transport。
  • 间接调用点:图片处理器、PDF 转换器、Webhook 投递器、云存储导入接口。

确认存在以下数据流时,应将其标记为重点审计对象:

用户输入或外部消息 → URL 拼接或解析 → 服务端 HTTP 客户端 → 响应内容返回或进入后续处理

如果请求参数来自管理员配置、第三方回调或消息队列,也不能直接视为可信。攻击者可能通过低权限功能影响这些数据,或者利用配置导入、模板渲染等间接路径触发请求。

二、审计中最容易被忽略的解析差异

很多修复只做了字符串前缀判断,例如判断 URL 是否以 开头。这类检查无法替代结构化解析,因为不同组件对用户名、端口、编码和重定向的解释可能不同。

重点关注以下情况:

  • 仅检查字符串前缀,没有校验主机名边界,导致相似域名被误认为可信。
  • 只允许某个域名,但请求库会自动跟随重定向,最终访问了未授权地址。
  • 校验域名时先解析一次,真正建立连接时再次解析,造成 DNS 解析结果不一致。
  • 允许用户提供端口,却没有限制端口范围。
  • 解析 IPv4 时忽略 IPv6,或者只拦截部分私有地址。
  • URL 中包含用户名、密码、编码字符或异常端口,导致校验组件和请求组件理解不同。
  • 代理配置、环境变量或自动发现机制改变了实际出站路径。

因此,审计结论不应只写“存在 SSRF”,而应记录:输入点、解析库、请求库、是否跟随重定向、DNS 解析时机、最终连接地址以及响应是否可被用户读取。

三、推荐的安全校验流程

对于确实需要访问远程资源的业务,建议将校验拆成几个独立阶段,并明确每个阶段的责任。

1. 只允许业务真正需要的协议

通常应只允许 https,必要时再按业务放开 http。应拒绝本地文件、脚本、数据以及其他非 HTTP 协议,并对协议名称做大小写归一化后再比较。

2. 解析 URL,而不是手工切字符串

使用成熟 URL 解析器提取 scheme、hostname、port、path 等字段。禁止出现用户名和密码时,应直接拒绝包含 userinfo 的 URL。主机名应进行规范化,避免把原始字符串直接送入后续判断。

3. 使用域名白名单,并进行边界匹配

如果业务只需访问固定站点,优先采用精确域名白名单。例如允许 api.example.test,而不是简单判断字符串是否包含 example.test。如果必须允许子域名,应明确记录规则,并对每次解析得到的 IP 做地址策略校验。

4. 校验解析结果和连接目标

对所有解析结果逐个检查,拒绝回环地址、链路本地地址、未指定地址、私有地址、保留地址以及企业明确禁止的网段。不能只检查第一个 A 记录,也不能忽略 AAAA 记录。

在高风险场景中,建议由受控出站代理统一完成 DNS 解析和连接,应用本身只提交经过校验的目标。这样可以减少 DNS 重绑定和不同语言运行时行为差异。

5. 默认关闭自动重定向

如果业务必须支持重定向,应限制最大跳转次数,并对每一次新的 Location 地址重复执行完整校验,包括协议、主机、端口和解析结果。不要只校验初始 URL。

四、一个更容易审计的实现示例

下面示例展示的是思路,重点是把策略集中在一个组件中,而不是在多个业务接口里分别拼接规则。示例中的域名和网段均为测试用途。

public final class SafeFetcher {
    private static final Set<String> ALLOWED_HOSTS =
        Set.of("files.example.test");

    public byte[] fetch(String rawUrl) throws IOException {
        URI uri = URI.create(rawUrl);

        if (!"https".equalsIgnoreCase(uri.getScheme())) {
            throw new SecurityException("scheme is not allowed");
        }
        if (uri.getUserInfo() != null || uri.getHost() == null) {
            throw new SecurityException("invalid authority");
        }
        if (!ALLOWED_HOSTS.contains(uri.getHost().toLowerCase(Locale.ROOT))) {
            throw new SecurityException("host is not allowlisted");
        }

        List<InetAddress> addresses =
            Arrays.asList(InetAddress.getAllByName(uri.getHost()));
        for (InetAddress address : addresses) {
            if (isBlockedAddress(address)) {
                throw new SecurityException("resolved address is blocked");
            }
        }

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

        HttpRequest request = HttpRequest.newBuilder(uri)
            .timeout(Duration.ofSeconds(5))
            .GET()
            .build();

        HttpResponse<byte[]> response =
            client.send(request, HttpResponse.BodyHandlers.ofByteArray());

        if (response.statusCode() >= 300 && response.statusCode() < 400) {
            throw new SecurityException("redirect is not allowed");
        }
        if (response.body().length > 5 * 1024 * 1024) {
            throw new IOException("response is too large");
        }
        return response.body();
    }
}

这个示例仍然不能直接视为完整的生产实现。尤其是 isBlockedAddress 需要覆盖 IPv4、IPv6、回环、链路本地、私有网段和保留地址;同时,应用网络层还应通过防火墙或 egress 策略限制不必要的出站访问。

五、如何在本地安全验证修复效果

测试 SSRF 修复时,建议搭建两个本地模拟服务:一个作为业务允许访问的资源服务,另一个作为不应访问的目标服务。通过日志观察请求是否到达,而不是尝试探测真实内网。

# 终端一:启动允许访问的测试服务
python3 -m http.server 18080 --bind 127.0.0.1

# 终端二:启动另一个本地服务,用于确认阻断逻辑
python3 -m http.server 18081 --bind 127.0.0.1

测试用例至少应覆盖:合法 HTTPS 地址、非允许域名、无端口和异常端口、解析到本地地址的域名、IPv6 地址、带用户名信息的 URL、返回 30x 的地址、连续重定向、超大响应体以及连接超时。

在单元测试中,可以将 DNS 解析器、HTTP Transport 和重定向处理抽象成接口,通过 mock 固定解析结果和响应,验证策略是否在请求建立前生效。这样既不会依赖外部网络,也能稳定复现边界条件。

六、不要忽略协议级和业务级风险

SSRF 的影响不只取决于“能不能访问某个地址”。还要检查响应内容是否会被原样返回、是否会被解析器继续处理,以及请求是否携带了内部凭据。

  • 禁止将云平台身份凭据、内部管理页面和服务发现接口暴露给不可信请求链路。
  • 对响应体设置大小、类型和处理时长限制,避免通过远程内容触发二次解析。
  • 图片、压缩包、XML、PDF 等内容进入解析器前,应单独评估文件解析漏洞和资源耗尽风险。
  • 日志中记录目标域名、解析地址、状态码、耗时和阻断原因,但不要记录完整的 Authorization、Cookie 或敏感查询参数。
  • 为出站请求设置独立网络权限,业务服务不应默认拥有访问整个内网的能力。

七、代码审计报告应如何落地

一份可执行的审计报告至少应包含以下内容:

  • 漏洞位置:控制器、服务类、任务处理器或第三方组件配置。
  • 可控路径:用户输入如何到达网络请求函数。
  • 实际行为:是否解析域名、是否跟随重定向、是否支持 IPv6、是否经过代理。
  • 影响边界:响应是否可见、是否携带认证信息、是否存在二次解析。
  • 复现方式:仅使用本地模拟服务说明请求是否产生,不提供针对真实系统的探测步骤。
  • 修复建议:协议白名单、目标白名单、地址校验、重定向策略、超时和响应大小限制。
  • 回归用例:覆盖正常、异常和边界输入,并在 CI 中持续执行。

总结

SSRF 防护的核心不是增加一条黑名单,而是建立从输入解析到网络出口的完整控制链。实践中最可靠的方向通常是:尽量缩小业务允许访问的目标范围,使用结构化 URL 解析,统一处理 DNS 和连接,关闭或严格校验重定向,同时在网络层实施默认拒绝的出站策略。

从审计角度看,真正有价值的结论应能回答三个问题:用户能控制哪些数据,服务端最终连接到了哪里,以及响应和凭据是否会跨越信任边界。围绕这三个问题展开代码追踪和本地验证,往往比单纯搜索危险函数更容易发现实际风险,也更便于开发团队完成修复和回归。

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

0篇意见

推荐意见

没有意见。

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

SiteMap HACKHAT © 2026 All rights reserved.