警惕递归处理不可信输入:从Protobuf漏洞看栈溢出攻击

本文深入分析递归函数处理不可信输入时导致的栈溢出漏洞,通过Google Protobuf Java库的CVE-2024-7254案例展示攻击原理,并提供使用CodeQL检测和深度计数器防护的具体方案,帮助开发者避免拒绝服务攻击。

不要对不可信输入使用递归 - Trail of Bits博客

Alexis Challande, Brad Swain
2025年2月21日
递归, 漏洞披露, Java

页面内容

单个恶意请求就能让使用递归函数处理不可信用户输入的Web应用崩溃。我们开发了一个简单的CodeQL查询来帮助发现栈溢出问题,并用它在多个知名Java项目中发现了拒绝服务(DoS)漏洞。所有这些项目都由具有安全意识、采用健全开发实践的组织维护:

  • ElasticSearch(PatternBank中的parseGeometryCollection)
  • OpenSearch(FilterPath中的parseGeometryCollection和validatePatternBank)
  • Protocol Buffers CVE-2024-7254
  • Guava Function重写
  • XStream CVE-2024-47072

我们的发现表明,递归虽然是一种强大的编程工具,但在需要可用性要求的应用中处理不可信数据时会变成严重的安全隐患。上述所有漏洞都已修复;然而,如果像这样的大规模项目都存在漏洞,你的代码中可能也有类似问题。继续阅读了解我们如何发现这些问题以及如何预防,或查看我们的完整白皮书。

递归的危害性

递归可以优雅、简单,最重要的是实用。它通常是处理嵌套结构的首选方法,无论是遍历树、访问图中的节点,还是解析像JSON这样的嵌套结构。

1
2
3
4
5
public int fibonacci(int n)  {
  if(n == 0) return 0;
  else if(n == 1) return 1;
  else return fibonacci(n - 1) + fibonacci(n - 2);
}

图1:来自Stack Overflow的递归斐波那契函数

然而,如果攻击者控制输入,通常很容易构造一个输入,在达到递归函数的基本情况之前耗尽栈空间。虽然开发者经常考虑防止无限递归,但通过提供单个恶意输入触发栈溢出来使应用崩溃是可能的。

1
2
3
4
Exception in thread "main" java.lang.StackOverflowError
    at Fibonacci.fibonacci(Fibonacci.java:8)
    at Fibonacci.fibonacci(Fibonacci.java:8)
    at Fibonacci.fibonacci(Fibonacci.java:8)

图2:来自Stack Overflow的StackOverflowError

虽然客户端崩溃可能只是不便,但服务器端崩溃即使有DDoS保护也可能使关键服务瘫痪。在需要可用性要求的应用中,这是一个具有实际危害潜力的真实风险。

Protobuf Java案例研究

为了说明这些漏洞如何在实践中显现,让我们看看我们在Google的protocol buffers(Protobuf)库中发现CVE-2024-7254的过程。这个问题展示了即使是有安全意识的组织也可能忽略递归处理漏洞。

根据Protobuf的官方文档:

Protocol buffers是Google的语言中立、平台中立、可扩展的序列化结构化数据机制——想象一下XML,但更小、更快、更简单。(来源)

解析不可信数据是出了名的棘手,安全研究人员已经针对每种格式的解析器进行了研究。Google开发了protocol buffers,以提供一种序列化交换格式,并在各种语言中自动生成解析器。它们在Google内部和更广泛的生态系统中被广泛使用。

然而,它们也容易受到递归错误攻击。

例如,攻击者可以通过发送这样一个消息来使使用protobuf-lite库解析外部消息的Java应用崩溃:

1
2
with open("recursive.data", "wb") as f:
    f.write(bytearray([19] * 5_000_000))

图3:Protobuf中的恶意消息

这个消息会抛出StackOverflowError。问题在于Protobuf如何解析未知字段。根据Protobuf文档:

未知字段是格式良好的protocol buffer序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件解析由具有新字段的新二进制文件发送的数据时,这些新字段在旧二进制文件中成为未知字段。

当这个问题与Groups(一个已弃用但由于向后兼容性仍被解析的功能)结合时,你会得到一个爆炸性的组合:

  1. 一个组可以包含另一个组。
  2. 如果被攻击的模式不包含组,新组将被解析为未知字段。
  3. 未知组可以包含另一个组。
  4. 转到第2步。

以下是负责解析的代码片段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
final boolean mergeOneFieldFrom(B unknownFields, Reader reader) throws IOException {
  int tag = reader.getTag();
  /* ... */
  switch (WireFormat.getTagWireType(tag)) {
    /* ... */
    case WireFormat.WIRETYPE_START_GROUP:
      final B subFields = newBuilder();
      /* ... */
      mergeFrom(subFields, reader);
      /* ... */
      return true;
    /* ... */
  }
}

final void mergeFrom(B unknownFields, Reader reader) throws IOException {
  while (true) {
    if (reader.getFieldNumber() == Reader.READ_DONE
        || !mergeOneFieldFrom(unknownFields, reader)) {
      break;
    }
  }
}

图4:Protobuf中的mergeFrom函数

这个漏洞的有趣之处在于,它对被攻击目标有一个前提条件:必须使用Protocol Buffer库的Java lite版本。对目标应用使用的模式没有要求。

虽然C++ API的官方文档出于安全原因建议丢弃未知字段,但它建议在解析消息之后进行。此时已经为时已晚。

虽然Protobuf解析通常对递归攻击具有弹性(使用深度计数器),但Google在开发过程中忘记了这个代码路径。我们负责任地向Google披露了这个问题,并被分配了CVE-2024-7254。

在调查这个问题时,我们发现它也适用于其他Protobuf实现,包括Rust-protobuf,这是一个用Rust编写的非官方Protocol buffers实现。

保护你的代码

随着软件系统越来越需要处理像JSON、XML和Protocol Buffers这样的嵌套数据格式,递归处理的风险已经增长。我们最初的研究主要关注Java项目,但对不可信输入进行递归的基本模式超越了语言边界,表明存在系统性的安全风险。

以下是保护应用的两个具体步骤:

  1. 审计你的代码。识别处理不可信数据的递归函数,并查找对嵌套数据格式的解析操作。特别注意处理反序列化的库代码。像我们的CodeQL查询这样的静态分析工具可以帮助简化审计过程。

  2. 实施安全措施。考虑迭代替代方案,为递归操作添加明确的深度限制,并在处理前验证输入大小和嵌套深度(如果可能)。

以下是添加深度计数器以防止恶意递归的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static final int MAX_DEPTH = 100;

public static int fibonacci(int n) throws InputTooBigException {
   return _fibonacci(n, 0);
}

public static int _fibonacci(int n, int depth) throws InputTooBigException  {
   if (depth >= MAX_DEPTH)
       throw new InputTooBigException();

   if(n == 0) return 0;
   else if(n == 1) return 1;
   else
       return _fibonacci(n-1, depth+1) + _fibonacci(n-2, depth+1);
}

图5:带有深度计数器的更新版斐波那契函数

了解更多

要深入了解我们的发现:

  • 阅读我们的白皮书《输入驱动递归:持续的安全风险》
  • 查看我们于2025年2月22日在华盛顿特区首届DistrictCon上的演讲
  • 尝试我们用于帮助发现有问题递归的CodeQL查询

如果你喜欢这篇文章,请分享: Twitter、LinkedIn、GitHub、Mastodon、Hacker News

页面内容

  • 递归的危害性
  • Protobuf Java案例研究
  • 保护你的代码
  • 了解更多

近期文章

  • 构建安全消息传递很难:对Bitchat安全辩论的细致看法
  • 使用Deptective调查你的依赖项
  • 系好安全带,Buttercup,AIxCC的评分回合正在进行中!
  • 使你的智能合约超越私钥风险成熟
  • Go解析器中意想不到的安全隐患

© 2025 Trail of Bits。 使用Hugo和Mainroad主题生成。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计