From 853a9ae1e33f9c906df7084d20a12eaf5c92ebe4 Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 22 Oct 2026 15:13:06 +0000 Subject: [PATCH] fix(preflight): decode an output-intent profile within 16 MiB Reading the colour space from an ICC header when /N is missing decoded the whole profile under the general 1 GiB stream limit to look at four bytes. The profile now decodes within 16 MiB; a larger one leaves the component count unknown. --- crates/preflight/src/lib.rs | 5 ++++- crates/preflight/src/tests.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/preflight/src/lib.rs b/crates/preflight/src/lib.rs index 2cda29fea1..ce17a4a8df 100644 --- a/crates/preflight/src/lib.rs +++ b/crates/preflight/src/lib.rs @@ -214,6 +214,9 @@ fn has_pdfa_intent(doc: &Document) -> bool { pdfa_intent_profile(doc).is_some() } +/// Largest output-intent profile decoded to read its header; real ICC profiles stay well under it. +const MAX_ICC_PROFILE: usize = 16 << 20; + /// The PDF/A output intent's destination profile: `Some(n)` with the profile's number of colour /// components (`/N`, else the ICC header's data colour space; `None` when neither says), or /// `None` when the document has no PDF/A output intent. @@ -229,7 +232,7 @@ fn pdfa_intent_profile(doc: &Document) -> Option> { let Object::Stream(s) = &*profile else { return Some(None) }; Some(s.dict.int(b"N").or_else(|| { // ICC.1 header: the data colour space is bytes 16–19. - match s.decoded().ok()?.get(16..20)? { + match s.decoded_within(MAX_ICC_PROFILE).ok()?.get(16..20)? { b"GRAY" => Some(1), b"RGB " => Some(3), b"CMYK" => Some(4), diff --git a/crates/preflight/src/tests.rs b/crates/preflight/src/tests.rs index 80b1c63c49..efd55f28e3 100644 --- a/crates/preflight/src/tests.rs +++ b/crates/preflight/src/tests.rs @@ -417,3 +417,15 @@ fn oversized_xmp_is_refused_while_decoding() { assert_eq!(stream.decoded().unwrap().len(), xmp::MAX_XMP_BYTES + 1, "fixture must actually decompress"); assert!(metadata(&doc).is_none(), "compressed metadata must be limited during decoding"); } + +#[test] +fn an_output_intent_profile_header_is_read_within_a_size_cap() { + let intent = "/OutputIntents [<< /S /GTS_PDFA1 /DestOutputProfile 2 0 R >>]"; + for (size, n) in [(128, Some(4)), (MAX_ICC_PROFILE + 1, None)] { + let mut doc = build(&[&format!("<< /Type /Catalog {intent} >>"), "null"], ""); + let mut profile = vec![0; size]; + profile[16..20].copy_from_slice(b"CMYK"); + doc.set(pdfcraft_cos::ObjRef::new(2, 0), Object::Stream(pdfcraft_cos::Stream::flate(Dict::new(), &profile))); + assert_eq!(pdfa_intent_profile(&doc), Some(n), "a {size}-byte profile"); + } +}