From 04a6224dabde929aa87261a7bd352fce20ec0296 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:06:22 +0000 Subject: [PATCH] Refuse a signed or malformed Content-Length in HTTP responses Found while testing crates/fetch/src/http.rs:598 (get) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/fetch/src/http.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/fetch/src/http.rs b/crates/fetch/src/http.rs index c95478388..3eca33df3 100644 --- a/crates/fetch/src/http.rs +++ b/crates/fetch/src/http.rs @@ -595,7 +595,10 @@ pub fn get(url: &Url, headers: &[(&str, String)], limits: &Limits) -> Result().map_err(|_| HttpError::Protocol(format!("bad Content-Length `{n}`")))?) + // RFC 9110: `1*DIGIT`; `u64::from_str` alone would also take a leading '+' + let v = n.trim(); + let len = if !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit()) { v.parse::().ok() } else { None }; + BodyMode::Length(len.ok_or_else(|| HttpError::Protocol(format!("bad Content-Length `{n}`")))?) } else { BodyMode::Close }; @@ -652,6 +655,29 @@ mod tests_connect { assert!(r.is_err()); assert!(t.elapsed() < Duration::from_millis(1500), "{:?}", t.elapsed()); } + + /// Content-Length is `1*DIGIT`: a sign, hex prefix, trailing junk or empty value is refused, not read as a length. + #[test] + fn signed_or_malformed_content_length_is_refused() { + for value in ["+3", "-3", "3x", "", "0x10", "+0"] { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let server = std::thread::spawn(move || { + let (mut s, _) = listener.accept().unwrap(); + let mut buf = [0u8; 1024]; + let _ = s.read(&mut buf); + let _ = s.write_all(format!("HTTP/1.1 200 OK\r\nContent-Length: {value}\r\n\r\nabc").as_bytes()); + }); + let cancel = AtomicBool::new(false); + let url = Url::parse(&format!("http://127.0.0.1:{port}/f")).unwrap(); + match get(&url, &[], &limits(&cancel)) { + Err(HttpError::Protocol(m)) => assert!(m.contains("bad Content-Length"), "{value:?}: {m}"), + Err(e) => panic!("{value:?}: unexpected error {e:?}"), + Ok(r) => panic!("{value:?}: malformed Content-Length accepted (status {})", r.status), + } + server.join().unwrap(); + } + } } #[cfg(test)]