diff --git a/src/parser.rs b/src/parser.rs index 0c135e8..364c878 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -102,6 +102,7 @@ pub fn parse_head(buf: &[u8], max_headers: usize) -> Result Result { + // Count occurrences; duplicates (even equal) are rejected + // post-loop. A single value must be one decimal integer — + // a comma-list ("5, 5") or non-numeric fails parse here. + cl_count += 1; content_length = Some( value.trim() .parse::() @@ -151,6 +156,13 @@ pub fn parse_head(buf: &[u8], max_headers: usize) -> Result 1 { + return Err(ParseError::BadContentLength); + } + if chunked { // Transfer-Encoding is an HTTP/1.1 mechanism; a 1.0 request // carrying it is malformed. And a request carrying BOTH a @@ -498,6 +510,36 @@ mod tests { } } + // --- Content-Length (RFC 9112 §6.3) --------------------------------- + + #[test] + fn parse_conflicting_content_length_is_rejected() { + // Two differing Content-Length values — classic CL.CL smuggling. + let req = b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\nContent-Length: 7\r\n\r\nhello!!"; + match parse_head(req, 64) { + Err(ParseError::BadContentLength) => {} + _ => panic!("expected BadContentLength for conflicting CL"), + } + } + + #[test] + fn parse_duplicate_equal_content_length_is_rejected() { + // Strict: even identical duplicates are rejected. + let req = b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello"; + match parse_head(req, 64) { + Err(ParseError::BadContentLength) => {} + _ => panic!("expected BadContentLength for duplicate CL"), + } + } + + #[test] + fn parse_single_content_length_still_ok() { + // Regression: the ordinary single-CL path is unchanged. + let req = b"POST / HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\nhello"; + let head = parse_head(req, 64).unwrap(); + assert_eq!(head.content_length, Some(5)); + } + #[test] fn serialise_basic_200() { let conn = Conn::new().put_status(200).put_body("hi");