Skip to content

Commit

Permalink
fix(http1): reject chunked headers missing a digit (hyperium#3494)
Browse files Browse the repository at this point in the history
Previously, hyper would decode `\r\n\r\n` as `0\r\n\r\n`. This fixes
hyper to require a digit to be present before starting at 0.

Reported-by: Ben Kallus <benjamin.p.kallus.gr@dartmouth.edu>
  • Loading branch information
seanmonstar authored and 0xE282B0 committed Jan 12, 2024
1 parent fd61709 commit a5321fc
Showing 1 changed file with 56 additions and 15 deletions.
71 changes: 56 additions & 15 deletions src/proto/h1/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ enum Kind {

#[derive(Debug, PartialEq, Clone, Copy)]
enum ChunkedState {
Start,
Size,
SizeLws,
Extension,
Expand All @@ -72,7 +73,7 @@ impl Decoder {

pub(crate) fn chunked() -> Decoder {
Decoder {
kind: Kind::Chunked(ChunkedState::Size, 0),
kind: Kind::Chunked(ChunkedState::new(), 0),
}
}

Expand Down Expand Up @@ -180,7 +181,22 @@ macro_rules! byte (
})
);

macro_rules! or_overflow {
($e:expr) => (
match $e {
Some(val) => val,
None => return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid chunk size: overflow",
))),
}
)
}

impl ChunkedState {
fn new() -> ChunkedState {
ChunkedState::Start
}
fn step<R: MemRead>(
&self,
cx: &mut Context<'_>,
Expand All @@ -190,6 +206,7 @@ impl ChunkedState {
) -> Poll<Result<ChunkedState, io::Error>> {
use self::ChunkedState::*;
match *self {
Start => ChunkedState::read_start(cx, body, size),
Size => ChunkedState::read_size(cx, body, size),
SizeLws => ChunkedState::read_size_lws(cx, body),
Extension => ChunkedState::read_extension(cx, body),
Expand All @@ -204,25 +221,46 @@ impl ChunkedState {
End => Poll::Ready(Ok(ChunkedState::End)),
}
}
fn read_size<R: MemRead>(

fn read_start<R: MemRead>(
cx: &mut Context<'_>,
rdr: &mut R,
size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> {
trace!("Read chunk hex size");
trace!("Read chunk start");

macro_rules! or_overflow {
($e:expr) => (
match $e {
Some(val) => val,
None => return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid chunk size: overflow",
))),
}
)
let radix = 16;
match byte!(rdr, cx) {
b @ b'0'..=b'9' => {
*size = or_overflow!(size.checked_mul(radix));
*size = or_overflow!(size.checked_add((b - b'0') as u64));
}
b @ b'a'..=b'f' => {
*size = or_overflow!(size.checked_mul(radix));
*size = or_overflow!(size.checked_add((b + 10 - b'a') as u64));
}
b @ b'A'..=b'F' => {
*size = or_overflow!(size.checked_mul(radix));
*size = or_overflow!(size.checked_add((b + 10 - b'A') as u64));
}
_ => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size line: missing size digit",
)));
}
}

Poll::Ready(Ok(ChunkedState::Size))
}

fn read_size<R: MemRead>(
cx: &mut Context<'_>,
rdr: &mut R,
size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> {
trace!("Read chunk hex size");

let radix = 16;
match byte!(rdr, cx) {
b @ b'0'..=b'9' => {
Expand Down Expand Up @@ -478,7 +516,7 @@ mod tests {
use std::io::ErrorKind::{InvalidData, InvalidInput, UnexpectedEof};

async fn read(s: &str) -> u64 {
let mut state = ChunkedState::Size;
let mut state = ChunkedState::new();
let rdr = &mut s.as_bytes();
let mut size = 0;
loop {
Expand All @@ -495,7 +533,7 @@ mod tests {
}

async fn read_err(s: &str, expected_err: io::ErrorKind) {
let mut state = ChunkedState::Size;
let mut state = ChunkedState::new();
let rdr = &mut s.as_bytes();
let mut size = 0;
loop {
Expand Down Expand Up @@ -532,6 +570,9 @@ mod tests {
// Missing LF or CRLF
read_err("F\rF", InvalidInput).await;
read_err("F", UnexpectedEof).await;
// Missing digit
read_err("\r\n\r\n", InvalidInput).await;
read_err("\r\n", InvalidInput).await;
// Invalid hex digit
read_err("X\r\n", InvalidInput).await;
read_err("1X\r\n", InvalidInput).await;
Expand Down

0 comments on commit a5321fc

Please sign in to comment.