Skip to content

Commit d71ff96

Browse files
authored
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 8291538 commit d71ff96

File tree

1 file changed

+77
-14
lines changed

1 file changed

+77
-14
lines changed

src/proto/h1/decode.rs

+77-14
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ use super::DecodedLength;
1111

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

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

7483
pub(crate) fn chunked() -> Decoder {
7584
Decoder {
76-
kind: Kind::Chunked(ChunkedState::new(), 0),
85+
kind: Kind::Chunked {
86+
state: ChunkedState::new(),
87+
chunk_len: 0,
88+
extensions_cnt: 0,
89+
},
7790
}
7891
}
7992

@@ -96,7 +109,12 @@ impl Decoder {
96109
pub(crate) fn is_eof(&self) -> bool {
97110
matches!(
98111
self.kind,
99-
Length(0) | Chunked(ChunkedState::End, _) | Eof(true)
112+
Length(0)
113+
| Chunked {
114+
state: ChunkedState::End,
115+
..
116+
}
117+
| Eof(true)
100118
)
101119
}
102120

@@ -127,11 +145,15 @@ impl Decoder {
127145
Poll::Ready(Ok(buf))
128146
}
129147
}
130-
Chunked(ref mut state, ref mut size) => {
148+
Chunked {
149+
ref mut state,
150+
ref mut chunk_len,
151+
ref mut extensions_cnt,
152+
} => {
131153
loop {
132154
let mut buf = None;
133155
// advances the chunked state
134-
*state = ready!(state.step(cx, body, size, &mut buf))?;
156+
*state = ready!(state.step(cx, body, chunk_len, extensions_cnt, &mut buf))?;
135157
if *state == ChunkedState::End {
136158
trace!("end of chunked");
137159
return Poll::Ready(Ok(Bytes::new()));
@@ -202,14 +224,15 @@ impl ChunkedState {
202224
cx: &mut Context<'_>,
203225
body: &mut R,
204226
size: &mut u64,
227+
extensions_cnt: &mut u64,
205228
buf: &mut Option<Bytes>,
206229
) -> Poll<Result<ChunkedState, io::Error>> {
207230
use self::ChunkedState::*;
208231
match *self {
209232
Start => ChunkedState::read_start(cx, body, size),
210233
Size => ChunkedState::read_size(cx, body, size),
211234
SizeLws => ChunkedState::read_size_lws(cx, body),
212-
Extension => ChunkedState::read_extension(cx, body),
235+
Extension => ChunkedState::read_extension(cx, body, extensions_cnt),
213236
SizeLf => ChunkedState::read_size_lf(cx, body, *size),
214237
Body => ChunkedState::read_body(cx, body, size, buf),
215238
BodyCr => ChunkedState::read_body_cr(cx, body),
@@ -306,6 +329,7 @@ impl ChunkedState {
306329
fn read_extension<R: MemRead>(
307330
cx: &mut Context<'_>,
308331
rdr: &mut R,
332+
extensions_cnt: &mut u64,
309333
) -> Poll<Result<ChunkedState, io::Error>> {
310334
trace!("read_extension");
311335
// We don't care about extensions really at all. Just ignore them.
@@ -320,7 +344,17 @@ impl ChunkedState {
320344
io::ErrorKind::InvalidData,
321345
"invalid chunk extension contains newline",
322346
))),
323-
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
347+
_ => {
348+
*extensions_cnt += 1;
349+
if *extensions_cnt >= CHUNKED_EXTENSIONS_LIMIT {
350+
Poll::Ready(Err(io::Error::new(
351+
io::ErrorKind::InvalidData,
352+
"chunk extensions over limit",
353+
)))
354+
} else {
355+
Poll::Ready(Ok(ChunkedState::Extension))
356+
}
357+
} // no supported extensions
324358
}
325359
}
326360
fn read_size_lf<R: MemRead>(
@@ -491,7 +525,6 @@ mod tests {
491525
}
492526
}
493527

494-
#[cfg(feature = "nightly")]
495528
impl MemRead for Bytes {
496529
fn read_mem(&mut self, _: &mut Context<'_>, len: usize) -> Poll<io::Result<Bytes>> {
497530
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) => {
@@ -632,6 +669,32 @@ mod tests {
632669
assert_eq!("1234567890abcdef", &result);
633670
}
634671

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

0 commit comments

Comments
 (0)