diff --git a/Cargo.lock b/Cargo.lock index dacc439..8c39ff5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,15 +32,15 @@ dependencies = [ [[package]] name = "alloc-no-stdlib" -version = "2.0.4" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" [[package]] name = "alloc-stdlib" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" dependencies = [ "alloc-no-stdlib", ] @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.4" +version = "9.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +checksum = "627b7cfb0578f0eafce087a90fcd40add1231900b69e4d4de2d456260a4f5d81" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "34c6783f42c21c952ad7112b354f0ba2f10760e9decea5b1b60523d6e270faaf" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1808,6 +1808,7 @@ version = "0.1.0-alpha.10" dependencies = [ "base64", "brotli", + "brotli-decompressor", "bytes", "flate2", "http", diff --git a/Cargo.toml b/Cargo.toml index 6af83fc..7291fb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,8 @@ turnloop-zstd-decoder = { path = "protocols/turnloop-zstd-decoder", version = "0 http = "1" url = "2" httparse = "1" -brotli = "8" +brotli-decompressor = "6.2" +brotli = "9.1.1" percent-encoding = "2" webpki-roots = "1" ring = "0.17" diff --git a/protocols/turnloop-http/Cargo.toml b/protocols/turnloop-http/Cargo.toml index ba8e004..3d0168a 100644 --- a/protocols/turnloop-http/Cargo.toml +++ b/protocols/turnloop-http/Cargo.toml @@ -29,13 +29,14 @@ http.workspace = true url.workspace = true httparse.workspace = true flate2.workspace = true -brotli.workspace = true +brotli-decompressor.workspace = true base64.workspace = true percent-encoding.workspace = true # Not the workspace entry, so that the host decides whether ring is linked. turnloop-tls = { path = "../turnloop-tls", version = "0.1.0-alpha.10", default-features = false } [dev-dependencies] +brotli.workspace = true rcgen.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/protocols/turnloop-http/src/compression.rs b/protocols/turnloop-http/src/compression.rs index 9aacb80..141b91e 100644 --- a/protocols/turnloop-http/src/compression.rs +++ b/protocols/turnloop-http/src/compression.rs @@ -55,8 +55,8 @@ enum Stage { } type BytePool = crate::recycling::Pool; type IntPool = crate::recycling::Pool; -type CodePool = crate::recycling::Pool; -type BrotliState = brotli::BrotliState; +type CodePool = crate::recycling::Pool; +type BrotliState = brotli_decompressor::BrotliState; // Keep the WASM decoder inline: 480 bytes avoids another per-response allocation. #[allow(clippy::large_enum_variant)] enum Engine { @@ -153,7 +153,11 @@ impl Codec { let bytes = BytePool::default(); let ints = IntPool::default(); let codes = CodePool::default(); - let state = Box::new(BrotliState::new(bytes.clone(), ints.clone(), codes.clone())); + let state = Box::new(BrotliState::new_strict( + bytes.clone(), + ints.clone(), + codes.clone(), + )); Engine::Brotli { state, bytes, @@ -209,7 +213,7 @@ impl Codec { ints, codes, } => { - **state = BrotliState::new(bytes.clone(), ints.clone(), codes.clone()); + **state = BrotliState::new_strict(bytes.clone(), ints.clone(), codes.clone()); } #[cfg(not(any(target_arch = "wasm32", feature = "pure-rust-zstd")))] Engine::Zstd { decoder, boundary } => { @@ -371,7 +375,7 @@ impl Codec { let mut available_in = input.len(); let mut available_out = output.len(); let mut total = self.total; - let result = brotli::BrotliDecompressStream( + let result = brotli_decompressor::BrotliDecompressStream( &mut available_in, &mut consumed, input, @@ -382,14 +386,16 @@ impl Codec { state, ); match result { - brotli::BrotliResult::ResultSuccess => { + brotli_decompressor::BrotliResult::ResultSuccess => { if consumed != input.len() { return Err(corrupt()); } finished = true; } - brotli::BrotliResult::ResultFailure => return Err(corrupt()), - brotli::BrotliResult::NeedsMoreInput if end => return Err(corrupt()), + brotli_decompressor::BrotliResult::ResultFailure => return Err(corrupt()), + brotli_decompressor::BrotliResult::NeedsMoreInput if end => { + return Err(corrupt()); + } _ => {} } } diff --git a/protocols/turnloop-http/src/recycling.rs b/protocols/turnloop-http/src/recycling.rs index 78312b8..f07882e 100644 --- a/protocols/turnloop-http/src/recycling.rs +++ b/protocols/turnloop-http/src/recycling.rs @@ -7,12 +7,12 @@ impl Default for Memory { Self(Vec::new()) } } -impl brotli::SliceWrapper for Memory { +impl brotli_decompressor::SliceWrapper for Memory { fn slice(&self) -> &[T] { &self.0 } } -impl brotli::SliceWrapperMut for Memory { +impl brotli_decompressor::SliceWrapperMut for Memory { fn slice_mut(&mut self) -> &mut [T] { &mut self.0 } @@ -28,7 +28,7 @@ impl Default for Pool { Self(Arc::new(Mutex::new(Vec::new()))) } } -impl brotli::Allocator for Pool { +impl brotli_decompressor::Allocator for Pool { type AllocatedMemory = Memory; fn alloc_cell(&mut self, len: usize) -> Memory { let mut pool = self.0.lock().expect("scratch pool poisoned"); diff --git a/protocols/turnloop-http/tests/codecs.rs b/protocols/turnloop-http/tests/codecs.rs index 749e6d6..e487848 100644 --- a/protocols/turnloop-http/tests/codecs.rs +++ b/protocols/turnloop-http/tests/codecs.rs @@ -431,6 +431,36 @@ fn gzip_deflate_br_zstd_decoding() { } } +/// Large-window Brotli is not RFC 7932 and not `Content-Encoding: br`: its +/// window can reach 1 GiB, which a tiny body could make the decoder allocate +/// before the decoded-size limit applies (the actix-http RUSTSEC-2026-0455 +/// shape). Node's decoder rejects it by default, and so does this one. +#[test] +fn large_window_brotli_is_rejected() { + use std::io::{Read, Write}; + let body = b"large window brotli is not content-encoding br"; + let params = brotli::enc::BrotliEncoderParams { + large_window: true, + lgwin: 30, + quality: 5, + ..Default::default() + }; + let mut encoded = Vec::new(); + { + let mut encoder = brotli::CompressorWriter::with_params(&mut encoded, 4096, ¶ms); + encoder.write_all(body).unwrap(); + } + // The stream is valid large-window Brotli: a permissive decoder reads it. + let mut permissive = Vec::new(); + brotli::Decompressor::new(encoded.as_slice(), 4096) + .read_to_end(&mut permissive) + .unwrap(); + assert_eq!(permissive, body); + + let mut out = Vec::new(); + assert!(turnloop_http::compression::decode("br", &encoded, &mut out, 1 << 20).is_err()); +} + #[test] fn vendored_hpack_interop_corpus() { use std::io::Read;