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.
This commit is contained in:
aiongg
2026-10-22 17:08:54 +00:00
parent a34697a8eb
commit 853a9ae1e3
2 changed files with 16 additions and 1 deletions
+4 -1
View File
@@ -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<Option<i64>> {
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),
+12
View File
@@ -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");
}
}