Skip to content

Commit 344a878

Browse files
committed
fix(http1): add internal limit for chunked extensions (#3495)
The chunked transfer-encoding allows for extensions within the header of each chunk. hyper currently ignores the extension bytes. Sending large amounts of bytes in the extensions will waste CPU reaing and skipping them. This change adds an internal limit to how many bytes will be read and ignored in a single body, before returning an error. Reported-by: Bartek Nowotarski <[email protected]>
1 parent 5eca028 commit 344a878

File tree

1 file changed

+78
-14
lines changed

1 file changed

+78
-14
lines changed

src/proto/h1/decode.rs

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ use super::DecodedLength;
1212

1313
use self::Kind::{Chunked, Eof, Length};
1414

15+
/// Maximum amount of bytes allowed in chunked extensions.
16+
///
17+
/// This limit is currentlty applied for the entire body, not per chunk.
18+
const CHUNKED_EXTENSIONS_LIMIT: u64 = 1024 * 16;
19+
1520
/// Decoders to handle different Transfer-Encodings.
1621
///
1722
/// If a message body does not include a Transfer-Encoding, it *should*
@@ -26,7 +31,11 @@ enum Kind {
2631
/// A Reader used when a Content-Length header is passed with a positive integer.
2732
Length(u64),
2833
/// A Reader used when Transfer-Encoding is `chunked`.
29-
Chunked(ChunkedState, u64),
34+
Chunked {
35+
state: ChunkedState,
36+
chunk_len: u64,
37+
extensions_cnt: u64,
38+
},
3039
/// A Reader used for responses that don't indicate a length or chunked.
3140
///
3241
/// The bool tracks when EOF is seen on the transport.
@@ -74,7 +83,11 @@ impl Decoder {
7483

7584
pub(crate) fn chunked() -> Decoder {
7685
Decoder {
77-
kind: Kind::Chunked(ChunkedState::new(), 0),
86+
kind: Kind::Chunked {
87+
state: ChunkedState::new(),
88+
chunk_len: 0,
89+
extensions_cnt: 0,
90+
},
7891
}
7992
}
8093

@@ -97,7 +110,12 @@ impl Decoder {
97110
pub(crate) fn is_eof(&self) -> bool {
98111
matches!(
99112
self.kind,
100-
Length(0) | Chunked(ChunkedState::End, _) | Eof(true)
113+
Length(0)
114+
| Chunked {
115+
state: ChunkedState::End,
116+
..
117+
}
118+
| Eof(true)
101119
)
102120
}
103121

@@ -128,11 +146,15 @@ impl Decoder {
128146
Poll::Ready(Ok(buf))
129147
}
130148
}
131-
Chunked(ref mut state, ref mut size) => {
149+
Chunked {
150+
ref mut state,
151+
ref mut chunk_len,
152+
ref mut extensions_cnt,
153+
} => {
132154
loop {
133155
let mut buf = None;
134156
// advances the chunked state
135-
*state = ready!(state.step(cx, body, size, &mut buf))?;
157+
*state = ready!(state.step(cx, body, chunk_len, extensions_cnt, &mut buf))?;
136158
if *state == ChunkedState::End {
137159
trace!("end of chunked");
138160
return Poll::Ready(Ok(Bytes::new()));
@@ -203,14 +225,15 @@ impl ChunkedState {
203225
cx: &mut Context<'_>,
204226
body: &mut R,
205227
size: &mut u64,
228+
extensions_cnt: &mut u64,
206229
buf: &mut Option<Bytes>,
207230
) -> Poll<Result<ChunkedState, io::Error>> {
208231
use self::ChunkedState::*;
209232
match *self {
210233
Start => ChunkedState::read_start(cx, body, size),
211234
Size => ChunkedState::read_size(cx, body, size),
212235
SizeLws => ChunkedState::read_size_lws(cx, body),
213-
Extension => ChunkedState::read_extension(cx, body),
236+
Extension => ChunkedState::read_extension(cx, body, extensions_cnt),
214237
SizeLf => ChunkedState::read_size_lf(cx, body, *size),
215238
Body => ChunkedState::read_body(cx, body, size, buf),
216239
BodyCr => ChunkedState::read_body_cr(cx, body),
@@ -307,6 +330,7 @@ impl ChunkedState {
307330
fn read_extension<R: MemRead>(
308331
cx: &mut Context<'_>,
309332
rdr: &mut R,
333+
extensions_cnt: &mut u64,
310334
) -> Poll<Result<ChunkedState, io::Error>> {
311335
trace!("read_extension");
312336
// We don't care about extensions really at all. Just ignore them.
@@ -321,7 +345,17 @@ impl ChunkedState {
321345
io::ErrorKind::InvalidData,
322346
"invalid chunk extension contains newline",
323347
))),
324-
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
348+
_ => {
349+
*extensions_cnt += 1;
350+
if *extensions_cnt >= CHUNKED_EXTENSIONS_LIMIT {
351+
Poll::Ready(Err(io::Error::new(
352+
io::ErrorKind::InvalidData,
353+
"chunk extensions over limit",
354+
)))
355+
} else {
356+
Poll::Ready(Ok(ChunkedState::Extension))
357+
}
358+
} // no supported extensions
325359
}
326360
}
327361
fn read_size_lf<R: MemRead>(
@@ -492,7 +526,6 @@ mod tests {
492526
}
493527
}
494528

495-
#[cfg(feature = "nightly")]
496529
impl MemRead for Bytes {
497530
fn read_mem(&mut self, _: &mut Context<'_>, len: usize) -> Poll<io::Result<Bytes>> {
498531
let n = std::cmp::min(len, self.len());
@@ -519,10 +552,12 @@ mod tests {
519552
let mut state = ChunkedState::new();
520553
let rdr = &mut s.as_bytes();
521554
let mut size = 0;
555+
let mut ext_cnt = 0;
522556
loop {
523-
let result =
524-
futures_util::future::poll_fn(|cx| state.step(cx, rdr, &mut size, &mut None))
525-
.await;
557+
let result = futures_util::future::poll_fn(|cx| {
558+
state.step(cx, rdr, &mut size, &mut ext_cnt, &mut None)
559+
})
560+
.await;
526561
let desc = format!("read_size failed for {:?}", s);
527562
state = result.expect(desc.as_str());
528563
if state == ChunkedState::Body || state == ChunkedState::EndCr {
@@ -536,10 +571,12 @@ mod tests {
536571
let mut state = ChunkedState::new();
537572
let rdr = &mut s.as_bytes();
538573
let mut size = 0;
574+
let mut ext_cnt = 0;
539575
loop {
540-
let result =
541-
futures_util::future::poll_fn(|cx| state.step(cx, rdr, &mut size, &mut None))
542-
.await;
576+
let result = futures_util::future::poll_fn(|cx| {
577+
state.step(cx, rdr, &mut size, &mut ext_cnt, &mut None)
578+
})
579+
.await;
543580
state = match result {
544581
Ok(s) => s,
545582
Err(e) => {
@@ -629,6 +666,33 @@ mod tests {
629666
assert_eq!("1234567890abcdef", &result);
630667
}
631668

669+
#[tokio::test]
670+
async fn test_read_chunked_extensions_over_limit() {
671+
// construct a chunked body where each individual chunked extension
672+
// is totally fine, but combined is over the limit.
673+
let per_chunk = super::CHUNKED_EXTENSIONS_LIMIT * 2 / 3;
674+
let mut scratch = vec![];
675+
for _ in 0..2 {
676+
scratch.extend(b"1;");
677+
scratch.extend(b"x".repeat(per_chunk as usize));
678+
scratch.extend(b"\r\nA\r\n");
679+
}
680+
scratch.extend(b"0\r\n\r\n");
681+
let mut mock_buf = Bytes::from(scratch);
682+
683+
let mut decoder = Decoder::chunked();
684+
let buf1 = decoder.decode_fut(&mut mock_buf).await.expect("decode1");
685+
assert_eq!(&buf1[..], b"A");
686+
687+
let err = decoder
688+
.decode_fut(&mut mock_buf)
689+
.await
690+
.expect_err("decode2");
691+
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
692+
assert_eq!(err.to_string(), "chunk extensions over limit");
693+
}
694+
695+
#[cfg(not(miri))]
632696
#[tokio::test]
633697
async fn test_read_chunked_trailer_with_missing_lf() {
634698
let mut mock_buf = &b"10\r\n1234567890abcdef\r\n0\r\nbad\r\r\n"[..];

0 commit comments

Comments
 (0)