The chunk-size line admits whitespace only before an extension

RFC 9112 7.1 allows whitespace before a chunk extension and nowhere
else. The reader trimmed both ends of the size, so " 3" and "3 " were
taken. It now splits at the first ';', trims trailing SP and HTAB only
when an extension follows, and leaves parse_digits as the digit
authority. Rows use an honest body so only the size line differs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-29 23:40:11 +02:00
commit bf5347c370

View file

@ -5813,10 +5813,16 @@ fn request_http_snapshot(target: SocketAddr, payload: &[u8]) -> Result<Vec<u8>,
let mut response = Vec::new();
loop {
let size = answer.read_line(ROUTE_HTTP_CHUNK_LINE_BYTES, body)?;
// RFC 9112 7.1: `chunk-size [ chunk-ext ] CRLF`, where an
// extension may follow whitespace. Nothing else is trimmed.
let size = std::str::from_utf8(&size)
.ok()
.and_then(|size| size.split(';').next())
.and_then(|size| parse_digits(size.trim_matches([' ', '\t']), 16))
.and_then(|line| match line.split_once(';') {
Some((digits, _extension)) => {
parse_digits(digits.trim_end_matches([' ', '\t']), 16)
}
None => parse_digits(line, 16),
})
.context("stable CultNet HTTP chunk size is invalid")
.map_err(refused)?;
if size == 0 {
@ -8786,9 +8792,15 @@ mod tests {
/// An honest chunked answer with `after_body` in place of its end.
fn chunked_honest_then(id: &str, after_body: &[u8]) -> Vec<u8> {
chunked_honest_sized(id, "", "", after_body)
}
/// An honest chunked answer whose chunk-size line is the hex length between
/// `before` and `after`.
fn chunked_honest_sized(id: &str, before: &str, after: &str, after_body: &[u8]) -> Vec<u8> {
let body = honest_body(id);
let mut reply = CHUNKED_HEAD.to_vec();
reply.extend_from_slice(format!("{:x}\r\n", body.len()).as_bytes());
reply.extend_from_slice(format!("{before}{:x}{after}\r\n", body.len()).as_bytes());
reply.extend_from_slice(&body);
reply.extend_from_slice(b"\r\n");
reply.extend_from_slice(after_body);
@ -8803,6 +8815,30 @@ mod tests {
challenge(scripted_peer(|id| chunked_honest_then(id, b"0\r\nT: 1\r\nU: 2\r\n\r\n")), |_| {})
.expect("an honest chunked answer with trailers");
// RFC 9112 7.1: whitespace may precede a chunk extension and nothing
// else. The answer is otherwise honest, so only the size line differs.
for (before, after) in [("", ";x"), ("", " ;x"), ("", "\t;x"), ("", ";x=1;y")] {
challenge(
scripted_peer(move |id| chunked_honest_sized(id, before, after, b"0\r\n\r\n")),
|_| {},
)
.unwrap_or_else(|error| panic!("size line {before:?}+size+{after:?}: {error:?}"));
}
for (why, before, after) in [
("whitespace before the chunk size", " ", ""),
("a tab before the chunk size", "\t", ""),
("whitespace after the chunk size and no extension", "", " "),
("whitespace before the size, ahead of an extension", " ", ";x"),
] {
assert_refused(
challenge(
scripted_peer(move |id| chunked_honest_sized(id, before, after, b"0\r\n\r\n")),
|_| {},
),
why,
);
}
// A trailer that begins and then the peer stops.
assert_refused(
challenge(scripted_peer(|id| chunked_honest_then(id, b"0\r\nT: 1\r\n")), |_| {}),