feat(parser): reject duplicate Content-Length (RFC 9112 §6.3)
The content-length arm ran content_length = Some(parse) per header, so a second Content-Length silently overwrote the first with no conflict check (CL.CL request smuggling; h1spec #21 -> 404 instead of 400). Count occurrences and reject any duplicate post-loop, strictly (even equal values), reusing BadContentLength (400). A single value is still required to be one decimal integer, so a comma-list or non-numeric keeps failing at parse as before. Tests: differing dup, equal dup, single-CL regression.
This commit is contained in:
@@ -102,6 +102,7 @@ pub fn parse_head(buf: &[u8], max_headers: usize) -> Result<ParsedHead, ParseErr
|
|||||||
let mut expect_100 = false;
|
let mut expect_100 = false;
|
||||||
let mut host_count = 0usize;
|
let mut host_count = 0usize;
|
||||||
let mut host_ok = true;
|
let mut host_ok = true;
|
||||||
|
let mut cl_count = 0usize;
|
||||||
|
|
||||||
for h in req.headers.iter() {
|
for h in req.headers.iter() {
|
||||||
let name_lower = h.name.to_ascii_lowercase();
|
let name_lower = h.name.to_ascii_lowercase();
|
||||||
@@ -109,6 +110,10 @@ pub fn parse_head(buf: &[u8], max_headers: usize) -> Result<ParsedHead, ParseErr
|
|||||||
|
|
||||||
match name_lower.as_str() {
|
match name_lower.as_str() {
|
||||||
"content-length" => {
|
"content-length" => {
|
||||||
|
// 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(
|
content_length = Some(
|
||||||
value.trim()
|
value.trim()
|
||||||
.parse::<usize>()
|
.parse::<usize>()
|
||||||
@@ -151,6 +156,13 @@ pub fn parse_head(buf: &[u8], max_headers: usize) -> Result<ParsedHead, ParseErr
|
|||||||
return Err(ParseError::Malformed);
|
return Err(ParseError::Malformed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Content-Length (RFC 9112 §6.3): more than one Content-Length is an
|
||||||
|
// unrecoverable framing ambiguity (CL.CL request smuggling). We are
|
||||||
|
// strict — reject any duplicate, not only differing values.
|
||||||
|
if cl_count > 1 {
|
||||||
|
return Err(ParseError::BadContentLength);
|
||||||
|
}
|
||||||
|
|
||||||
if chunked {
|
if chunked {
|
||||||
// Transfer-Encoding is an HTTP/1.1 mechanism; a 1.0 request
|
// Transfer-Encoding is an HTTP/1.1 mechanism; a 1.0 request
|
||||||
// carrying it is malformed. And a request carrying BOTH a
|
// 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]
|
#[test]
|
||||||
fn serialise_basic_200() {
|
fn serialise_basic_200() {
|
||||||
let conn = Conn::new().put_status(200).put_body("hi");
|
let conn = Conn::new().put_status(200).put_body("hi");
|
||||||
|
|||||||
Reference in New Issue
Block a user