From a4b5cfd34681ebf49370c27b3002782dec813482 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Fri, 9 Oct 2026 16:59:31 +0200 Subject: [PATCH] fix: reject chunked GET and HEAD bodies --- parser/fuzz/tests/conformance.rs | 10 ++++++++++ parser/src/parse.rs | 5 ++++- parser/tests/compliance.rs | 17 +++++++++++++++++ parser/wasm/test/compliance.test.js | 14 ++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/parser/fuzz/tests/conformance.rs b/parser/fuzz/tests/conformance.rs index 6f4cf65..959ba38 100644 --- a/parser/fuzz/tests/conformance.rs +++ b/parser/fuzz/tests/conformance.rs @@ -57,6 +57,16 @@ fn rejects_transfer_encoding_not_ending_in_chunked() { } } +#[test] +fn rejects_chunked_get_and_head_requests() { + for input in [ + &b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n"[..], + &b"HEAD / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nx\r\n0\r\n\r\n"[..], + ] { + assert_ne!(error_code(input, true), ERROR_NONE, "chunked GET/HEAD must be rejected: {input:?}"); + } +} + // --- Well-formed messages must still be accepted. --- #[test] diff --git a/parser/src/parse.rs b/parser/src/parse.rs index 80105a0..bdb1bbc 100644 --- a/parser/src/parse.rs +++ b/parser/src/parse.rs @@ -836,7 +836,10 @@ impl Parser { UNEXPECTED_TRAILERS, "Trailers are not allowed when not using chunked transfer encoding" ); - } else if self.is_request && (method == METHOD_GET || method == METHOD_HEAD) && self.content_length > 0 { + } else if self.is_request + && (method == METHOD_GET || method == METHOD_HEAD) + && (self.content_length > 0 || self.has_transfer_encoding) + { fail!(UNEXPECTED_CONTENT, "Unexpected content for the request (GET or HEAD)"); } diff --git a/parser/tests/compliance.rs b/parser/tests/compliance.rs index fb1244a..e129465 100644 --- a/parser/tests/compliance.rs +++ b/parser/tests/compliance.rs @@ -196,6 +196,23 @@ fn compliance_head_body_rejected() { assert_error(&parser); } +// Milo rejects chunked framing on GET and HEAD even when the encoded body is empty. +#[test] +fn compliance_get_head_chunked_body_rejected() { + for method in ["GET", "HEAD"] { + for body in ["0\r\n\r\n", "1\r\nx\r\n0\r\n\r\n"] { + let mut parser = request_parser(); + let message = wire(&format!( + "{method} / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n{body}" + )); + + parse(&mut parser, &message); + + assert_error(&parser); + } + } +} + // Methods other than GET and HEAD can carry valid body framing. #[test] fn compliance_post_body_accepted() { diff --git a/parser/wasm/test/compliance.test.js b/parser/wasm/test/compliance.test.js index a0cd1bf..e104ab8 100644 --- a/parser/wasm/test/compliance.test.js +++ b/parser/wasm/test/compliance.test.js @@ -131,6 +131,20 @@ it('compliance_head_body_rejected', t => { assertError(milo, parser) }) +// Milo rejects chunked framing on GET and HEAD even when the encoded body is empty. +for (const method of ['GET', 'HEAD']) { + for (const [name, body] of [ + ['empty', '0\r\n\r\n'], + ['non_empty', '1\r\nx\r\n0\r\n\r\n'] + ]) { + it(`compliance_${method.toLowerCase()}_${name}_chunked_body_rejected`, t => { + const { milo, parser, parse } = requestParser(t) + parse(`${method} / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n${body}`) + assertError(milo, parser) + }) + } +} + // Methods other than GET and HEAD can carry valid body framing. it('compliance_post_body_accepted', t => { const { milo, parser, parse } = requestParser(t)