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
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-12 06:27:29 -05:00
1 parent 23465223bc
commit eb0ea78d9a
3 files changed
+56

No files matched your search

+23
View File
@@ -433,9 +433,32 @@ pub fn decode_rgba(bytes: &[u8]) -> Option<image::RgbaImage> {
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<Vec<u8>> {
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()
}
+27
View File
@@ -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<u8> {
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])])));
}
+6
View File
@@ -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<Vec<u8>> {
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<Vec<u8>>, 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;
};