From eb0ea78d9ad8fbea22361c1812f55303fbabe550 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 06:27:29 -0500 Subject: [PATCH] Images: a WebP with an ICC chunk longer than the file is read without its profile * Images: a WebP whose ICC chunk declares more than the file holds is read without it --- crates/images/src/lib.rs | 23 +++++++++++++++++++++++ crates/images/src/tests.rs | 27 +++++++++++++++++++++++++++ crates/pdf/src/export/images.rs | 6 ++++++ 3 files changed, 56 insertions(+) diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index 83c540e6e..98c058aa6 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -433,9 +433,32 @@ pub fn decode_rgba(bytes: &[u8]) -> Option { to_display_rgba(px, Some(&icc), &DisplayColour::active()) } +/// False when `bytes` is a WebP whose `ICCP` chunk declares more bytes than the file holds or than +/// [`colour::MAX_ICC_BYTES`]. The decoder sizes its buffer from the declared length before it reads +/// anything, so such a file must not reach `icc_profile()`; its pixels decode without it. +pub fn webp_icc_is_sane(bytes: &[u8]) -> bool { + if bytes.get(0..4) != Some(b"RIFF") || bytes.get(8..12) != Some(b"WEBP") { + return true; + } + let mut pos = 12usize; + while let Some(head) = bytes.get(pos..pos.saturating_add(8)) { + let size = u32::from_le_bytes([head[4], head[5], head[6], head[7]]) as usize; + let body = pos + 8; + if &head[..4] == b"ICCP" { + return size <= colour::MAX_ICC_BYTES && body.saturating_add(size) <= bytes.len(); + } + // Chunks are padded to an even size. + pos = body.saturating_add(size).saturating_add(size & 1); + } + true +} + /// The ICC profile embedded in a PNG, JPEG, TIFF or WebP. fn embedded_icc(bytes: &[u8]) -> Option> { use image::ImageDecoder; + if !webp_icc_is_sane(bytes) { + return None; + } let mut d = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?.into_decoder().ok()?; d.icc_profile().ok().flatten() } diff --git a/crates/images/src/tests.rs b/crates/images/src/tests.rs index 3e5daf8ab..cd577f09a 100644 --- a/crates/images/src/tests.rs +++ b/crates/images/src/tests.rs @@ -534,3 +534,30 @@ fn gif_tone_and_cmyk_inks_check_the_rgba_budget() { assert!(too_large_to_decode(&tiff)); assert_eq!(decode_cmyk_tiff(&tiff), None); } + +fn webp_with(chunks: &[(&[u8; 4], u32, &[u8])]) -> Vec { + let mut v = b"RIFF\0\0\0\0WEBP".to_vec(); + for (id, declared, body) in chunks { + v.extend_from_slice(*id); + v.extend_from_slice(&declared.to_le_bytes()); + v.extend_from_slice(body); + if body.len() % 2 == 1 { + v.push(0); + } + } + v +} + +/// A WebP that declares an `ICCP` chunk larger than the file (or than the ICC cap) never reaches the +/// decoder's profile read, which would size a buffer from the declared length. +#[test] +fn a_webp_icc_chunk_that_declares_more_than_it_holds_is_refused_before_decoding() { + assert!(webp_icc_is_sane(&webp_with(&[(b"VP8X", 10, &[0; 10]), (b"ICCP", 4, &[1; 4])]))); + // An odd-sized chunk before it is padded to even. + assert!(webp_icc_is_sane(&webp_with(&[(b"EXIF", 3, &[0; 3]), (b"ICCP", 4, &[1; 4])]))); + assert!(!webp_icc_is_sane(&webp_with(&[(b"VP8X", 10, &[0; 10]), (b"ICCP", u32::MAX, &[1; 4])]))); + assert!(!webp_icc_is_sane(&webp_with(&[(b"ICCP", 100, &[1; 4])]))); + assert!(embedded_icc(&webp_with(&[(b"ICCP", u32::MAX, &[1; 4])])).is_none()); + assert!(webp_icc_is_sane(b"\x89PNG not a webp")); + assert!(webp_icc_is_sane(&webp_with(&[(b"VP8L", 6, &[0; 6])]))); +} diff --git a/crates/pdf/src/export/images.rs b/crates/pdf/src/export/images.rs index ec7c76fd6..b50c6b20a 100644 --- a/crates/pdf/src/export/images.rs +++ b/crates/pdf/src/export/images.rs @@ -47,6 +47,9 @@ pub(super) struct RasterMode { /// The ICC profile embedded in an image file, if it has one. pub(super) fn embedded_icc(data: &[u8]) -> Option> { use image::ImageDecoder; + if !designcraft_images::webp_icc_is_sane(data) { + return None; + } let mut dec = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format().ok()?.into_decoder().ok()?; dec.icc_profile().ok().flatten() } @@ -89,6 +92,9 @@ pub(super) fn jpeg(data: krilla::Data, icc: impl FnOnce() -> Option>, mo /// its own. fn untagged_rgb(data: &[u8]) -> bool { use image::ImageDecoder; + if !designcraft_images::webp_icc_is_sane(data) { + return false; + } let Some(mut dec) = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format().ok().and_then(|r| r.into_decoder().ok()) else { return false; };