From d6b3fb96718ff16c2e1bd2a5eb0526ec6e2cffd7 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 21:54:26 -0400 Subject: [PATCH] security: bound vendored hayro ASCII85 output by the stream decode limit (#253) * security: bound ASCII85 output by the stream decode limit (CodeRabbit) * fix(ascii85): truncate terminal partial groups to decode limit --- vendor/hayro-syntax/src/filter/ascii_85.rs | 28 ++++++++++++++++++++-- vendor/hayro-syntax/src/filter/mod.rs | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/vendor/hayro-syntax/src/filter/ascii_85.rs b/vendor/hayro-syntax/src/filter/ascii_85.rs index 5a18cf83..b7199365 100644 --- a/vendor/hayro-syntax/src/filter/ascii_85.rs +++ b/vendor/hayro-syntax/src/filter/ascii_85.rs @@ -4,7 +4,9 @@ use crate::reader::Reader; use crate::trivia::is_white_space_character; use alloc::vec::Vec; -pub(crate) fn decode(data: &[u8]) -> Option> { +/// PdfCraft patch: `limit` is the most bytes produced (see `filter::decode_limit`); each `z` costs +/// one input byte and yields four, so a Flate stream of them would outgrow the budget. +pub(crate) fn decode(data: &[u8], limit: usize) -> Option> { const POW_85: [u32; 5] = [52200625, 614125, 7225, 85, 1]; let mut reader = Reader::new(data); @@ -55,13 +57,21 @@ pub(crate) fn decode(data: &[u8]) -> Option> { Some(()) }; - let mut decoded = Vec::with_capacity(data.len() * 4 / 5); + let mut decoded = Vec::with_capacity((data.len() * 4 / 5).min(limit)); let mut group = Vec::with_capacity(5); loop { + if decoded.len() >= limit { + decoded.truncate(limit); + warn!("ascii85 stream stopped at its decode limit of {limit} bytes"); + + return Some(decoded); + } + let Some(b) = read_byte() else { // Be lenient and accept what we have (see PDFBOX-5910). flush_group(&mut group, &mut decoded)?; + decoded.truncate(limit); return Some(decoded); }; @@ -82,6 +92,7 @@ pub(crate) fn decode(data: &[u8]) -> Option> { // Technically requires a '>', but there is a PDF where it isn't // appended and decodes fine in other viewers. flush_group(&mut group, &mut decoded)?; + decoded.truncate(limit); return Some(decoded); } @@ -89,3 +100,16 @@ pub(crate) fn decode(data: &[u8]) -> Option> { } } } + +#[cfg(test)] +mod tests { + use super::decode; + + #[test] + fn output_stops_at_the_limit() { + assert_eq!(decode(&[b'z'; 100], 10).unwrap().len(), 10); + assert_eq!(decode(b"zz~>", 100).unwrap(), [0; 8]); + assert_eq!(decode(b"!!!!~>", 2).unwrap().len(), 2); + assert_eq!(decode(b"!!!!", 2).unwrap().len(), 2); + } +} diff --git a/vendor/hayro-syntax/src/filter/mod.rs b/vendor/hayro-syntax/src/filter/mod.rs index 90ffd76e..e3622795 100644 --- a/vendor/hayro-syntax/src/filter/mod.rs +++ b/vendor/hayro-syntax/src/filter/mod.rs @@ -95,7 +95,7 @@ impl Filter { Self::AsciiHexDecode => ascii_hex::decode(data) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), - Self::Ascii85Decode => ascii_85::decode(data) + Self::Ascii85Decode => ascii_85::decode(data, limit) .map(FilterResult::from_data) .ok_or(DecodeFailure::StreamDecode), Self::RunLengthDecode => run_length::decode(data, limit)