mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 11:37:59 -06:00
fix(redact): bound image decoding by the declared raster size
Clearing the pixels of a partly covered image decoded its stream with the global 1 GiB allowance and only then cut it to the raster length its Width, Height and bit depth declare. A small Flate stream could make every such image allocate and inflate far more than it can use. Decoding now stops at twice the raster plus a byte per row (filter-chain and predictor overhead) and a little slack; an image past that is removed whole, as an undecodable one already is.
This commit is contained in:
@@ -77,7 +77,11 @@ fn decode_and_clear(doc: &Document, s: &Stream, ctm: &Matrix, rects: &[[f64; 4]]
|
||||
let bytes = row.checked_mul(h)?;
|
||||
let row_bits = row.checked_mul(8)?;
|
||||
bytes.checked_mul(8)?;
|
||||
let mut data = s.decoded().ok()?;
|
||||
// Decode no further than the raster can need: a filter stage may legitimately produce up to
|
||||
// twice it (ASCIIHex text under Flate) plus a PNG predictor's byte per row. Data past that
|
||||
// can only be a decompression bomb; the image is then removed whole.
|
||||
let allowance = bytes.checked_add(h)?.checked_mul(2)?.checked_add(4096)?;
|
||||
let mut data = s.decoded_within(allowance).ok()?;
|
||||
if data.len() < bytes {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2461,6 +2461,7 @@ fn unused_descriptions_in_shared_or_inherited_resources_are_retired() {
|
||||
}
|
||||
}
|
||||
|
||||
mod limits;
|
||||
mod property_retirement;
|
||||
mod surviving_references;
|
||||
mod text_state;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//! Work and memory stay bounded by what the page declares, whatever the file holds.
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A half-covered 4×1 grey image whose Flate data decodes to `decoded`.
|
||||
fn half_covered_image(decoded: &[u8]) -> Report {
|
||||
let dict = "/Type /XObject /Subtype /Image /Width 4 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8";
|
||||
let mut doc = one_page(b"q 40 0 0 10 100 100 cm /Im Do Q", "/XObject << /Im 7 0 R >>", vec![stream(dict, b"")]);
|
||||
let Object::Stream(s) = &*doc.get(ObjRef::new(7, 0)) else { panic!() };
|
||||
doc.set(ObjRef::new(7, 0), Object::Stream(Stream::flate(s.dict.clone(), decoded)));
|
||||
mark(&mut doc, 0, &[[95.0, 95.0, 120.0, 115.0]], "");
|
||||
apply(&mut doc, None).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_image_decoding_far_past_its_raster_is_removed_without_decoding_it_all() {
|
||||
let r = half_covered_image(&[255; 4]);
|
||||
assert_eq!((r.images_removed, r.images_cleared), (0, 1), "{r:?}");
|
||||
// 4 bytes of raster, then 4 MiB the image never uses: decoding stops at a bound derived from
|
||||
// the raster, and the image goes whole.
|
||||
let mut bomb = vec![255; 4];
|
||||
bomb.resize(4 << 20, 0);
|
||||
let r = half_covered_image(&bomb);
|
||||
assert_eq!((r.images_removed, r.images_cleared), (1, 0), "{r:?}");
|
||||
}
|
||||
Reference in New Issue
Block a user