Skip to content

Commit 411d059

Browse files
authored
fix: limit QPACK header list size during decoding (#3618)
* fix: limit QPACK header list size during decoding A maliciously crafted header block with many indexed references to large dynamic table entries could use lots of memory, as `decode_header_block` had no bound on the number of headers it would accumulate. Cap the total uncompressed header list size to `LiteralReader::MAX_LEN` using a `checked_sub` budget, consistent with RFC 9113 §6.5.2. Fixes #3616. * Improve coverage
1 parent 7c2a6a3 commit 411d059

2 files changed

Lines changed: 71 additions & 28 deletions

File tree

neqo-qpack/src/header_block.rs

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use crate::{
2222
NO_PREFIX,
2323
},
2424
qpack_send_buf::Encoder as _,
25-
reader::{ReceiverBufferWrapper, parse_utf8},
26-
table::HeaderTable,
25+
reader::{LiteralReader, ReceiverBufferWrapper, parse_utf8},
26+
table::{ADDITIONAL_TABLE_ENTRY_SIZE, HeaderTable},
2727
};
2828

2929
#[derive(Default, Debug, PartialEq, Eq)]
@@ -210,47 +210,42 @@ impl<'a> HeaderDecoder<'a> {
210210
return Ok(HeaderDecoderResult::Blocked(self.req_insert_cnt));
211211
}
212212
let mut h: Vec<Header> = Vec::new();
213+
let mut remaining = LiteralReader::MAX_LEN;
213214

214215
while !self.buf.done() {
215216
let b = Error::map_error(self.buf.peek(), Error::Decompression)?;
216-
if HEADER_FIELD_INDEX_STATIC.cmp_prefix(b) {
217-
h.push(Error::map_error(
218-
self.read_indexed_static(),
219-
Error::Decompression,
220-
)?);
217+
let header = if HEADER_FIELD_INDEX_STATIC.cmp_prefix(b) {
218+
Error::map_error(self.read_indexed_static(), Error::Decompression)?
221219
} else if HEADER_FIELD_INDEX_DYNAMIC.cmp_prefix(b) {
222-
h.push(Error::map_error(
223-
self.read_indexed_dynamic(table),
224-
Error::Decompression,
225-
)?);
220+
Error::map_error(self.read_indexed_dynamic(table), Error::Decompression)?
226221
} else if HEADER_FIELD_INDEX_DYNAMIC_POST.cmp_prefix(b) {
227-
h.push(Error::map_error(
228-
self.read_indexed_dynamic_post(table),
229-
Error::Decompression,
230-
)?);
222+
Error::map_error(self.read_indexed_dynamic_post(table), Error::Decompression)?
231223
} else if HEADER_FIELD_LITERAL_NAME_REF_STATIC.cmp_prefix(b) {
232-
h.push(Error::map_error(
224+
Error::map_error(
233225
self.read_literal_with_name_ref_static(),
234226
Error::Decompression,
235-
)?);
227+
)?
236228
} else if HEADER_FIELD_LITERAL_NAME_REF_DYNAMIC.cmp_prefix(b) {
237-
h.push(Error::map_error(
229+
Error::map_error(
238230
self.read_literal_with_name_ref_dynamic(table),
239231
Error::Decompression,
240-
)?);
232+
)?
241233
} else if HEADER_FIELD_LITERAL_NAME_LITERAL.cmp_prefix(b) {
242-
h.push(Error::map_error(
243-
self.read_literal_with_name_literal(),
244-
Error::Decompression,
245-
)?);
234+
Error::map_error(self.read_literal_with_name_literal(), Error::Decompression)?
246235
} else if HEADER_FIELD_LITERAL_NAME_REF_DYNAMIC_POST.cmp_prefix(b) {
247-
h.push(Error::map_error(
236+
Error::map_error(
248237
self.read_literal_with_name_ref_dynamic_post(table),
249238
Error::Decompression,
250-
)?);
239+
)?
251240
} else {
252241
unreachable!("All prefixes are covered");
253-
}
242+
};
243+
remaining = remaining
244+
.checked_sub(
245+
header.name().len() + header.value().len() + ADDITIONAL_TABLE_ENTRY_SIZE,
246+
)
247+
.ok_or(Error::Decompression)?;
248+
h.push(header);
254249
}
255250

256251
qtrace!("[{self}] done decoding header block");
@@ -393,7 +388,10 @@ impl<'a> HeaderDecoder<'a> {
393388
#[cfg_attr(coverage_nightly, coverage(off))]
394389
mod tests {
395390

396-
use super::{HeaderDecoder, HeaderDecoderResult, HeaderEncoder, HeaderTable};
391+
use super::{
392+
ADDITIONAL_TABLE_ENTRY_SIZE, HeaderDecoder, HeaderDecoderResult, HeaderEncoder,
393+
HeaderTable, LiteralReader,
394+
};
397395
use crate::Error;
398396

399397
const INDEX_STATIC_TEST: &[(u64, &[u8], &str, &str)] = &[
@@ -927,4 +925,49 @@ mod tests {
927925
decoder_h.decode_header_block(&table, 1000, 0).unwrap_err()
928926
);
929927
}
928+
929+
/// A header block that references the same static table entry many times must be
930+
/// rejected before exhausting memory.
931+
#[test]
932+
fn header_list_size_limit() {
933+
let table = HeaderTable::new(false);
934+
// 0xC0 = HEADER_FIELD_INDEX_STATIC with index 0 (:authority, "").
935+
let entry_size = ":authority".len() + ADDITIONAL_TABLE_ENTRY_SIZE;
936+
let reps = LiteralReader::MAX_LEN / entry_size + 1;
937+
// Prefix: required_insert_cnt=0, base_delta=0 (2 bytes: 0x00, 0x00)
938+
let mut buf = vec![0x00u8, 0x00];
939+
buf.resize(buf.len() + reps, 0xC0);
940+
let mut decoder_h = HeaderDecoder::new(&buf);
941+
assert_eq!(
942+
Error::Decompression,
943+
decoder_h.decode_header_block(&table, 1000, 0).unwrap_err()
944+
);
945+
}
946+
947+
/// Truncated input for each header-block prefix type must propagate as
948+
/// `Error::Decompression`. Each byte sets the maximum value for its prefix's
949+
/// integer field, forcing a continuation byte that is not present.
950+
#[test]
951+
fn decode_truncated_for_each_prefix() {
952+
const TRUNCATED_PREFIXES: &[u8] = &[
953+
0xFF, // HEADER_FIELD_INDEX_STATIC
954+
0xBF, // HEADER_FIELD_INDEX_DYNAMIC
955+
0x1F, // HEADER_FIELD_INDEX_DYNAMIC_POST
956+
0x5F, // HEADER_FIELD_LITERAL_NAME_REF_STATIC
957+
0x4F, // HEADER_FIELD_LITERAL_NAME_REF_DYNAMIC
958+
0x3F, // HEADER_FIELD_LITERAL_NAME_LITERAL
959+
0x07, // HEADER_FIELD_LITERAL_NAME_REF_DYNAMIC_POST
960+
];
961+
let mut table = HeaderTable::new(false);
962+
fill_table(&mut table);
963+
for &prefix in TRUNCATED_PREFIXES {
964+
let buf = [0x00, 0x00, prefix];
965+
let mut decoder_h = HeaderDecoder::new(&buf);
966+
assert_eq!(
967+
Error::Decompression,
968+
decoder_h.decode_header_block(&table, 1000, 0).unwrap_err(),
969+
"prefix {prefix:#04x}"
970+
);
971+
}
972+
}
930973
}

neqo-qpack/src/reader.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ impl LiteralReader {
266266
/// The Gecko limit is in `network.http.max_response_header_size` and defaults to
267267
/// 393216 bytes (384 KB), see `modules/libpref/init/StaticPrefList.yaml`. We use
268268
/// the same limit.
269-
const MAX_LEN: usize = 384 * 1024;
269+
pub(crate) const MAX_LEN: usize = 384 * 1024;
270270

271271
/// Creates `LiteralReader` with the first byte. This constructor is always used
272272
/// when a literal has a prefix.

0 commit comments

Comments
 (0)