From 58cf0ba28afa3f6c8df55ed148bd6f5fee706d17 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 09:08:51 -0800 Subject: [PATCH] Place: WebP EXIF stops at the declared RIFF end The RIFF size counts the bytes after its own field, so the container ends at offset 8 plus the size, not 12. An EXIF chunk that ran up to four bytes past the declared container was read when trailing bytes followed it. A new test covers an exact size with and without trailing bytes, a size declared four bytes short, and a file cut inside the EXIF chunk. --- crates/images/src/resolution.rs | 3 ++- crates/images/src/tests.rs | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/images/src/resolution.rs b/crates/images/src/resolution.rs index ea0a55eaa..3b04d8e73 100644 --- a/crates/images/src/resolution.rs +++ b/crates/images/src/resolution.rs @@ -203,8 +203,9 @@ fn png_resolution(bytes: &[u8]) -> Option<(f64, f64)> { /// A WebP EXIF chunk carries a TIFF header: reuse the TIFF resolution tags. fn webp_resolution(bytes: &[u8]) -> Option<(f64, f64)> { + // The RIFF size counts the bytes after the size field itself (offset 8). let riff = u32::from_le_bytes(bytes.get(4..8)?.try_into().ok()?) as usize; - let end = 12usize.checked_add(riff)?.min(bytes.len()); + let end = 8usize.checked_add(riff)?.min(bytes.len()); let mut pos = 12; loop { let kind = bytes.get(pos..pos + 4)?; diff --git a/crates/images/src/tests.rs b/crates/images/src/tests.rs index 1803b310a..d7d46e4a4 100644 --- a/crates/images/src/tests.rs +++ b/crates/images/src/tests.rs @@ -676,6 +676,40 @@ fn webp_exif_after_many_frames_is_read() { assert_eq!(raster_resolution(&webp), Some((300.0, 300.0))); } +/// The RIFF size counts the bytes after its own field (offset 8): an EXIF chunk that runs past +/// the declared container is not read, even when trailing bytes follow the container. +#[test] +fn webp_exif_past_the_declared_riff_end_is_not_read() { + let mut body = b"WEBP".to_vec(); + body.extend_from_slice(b"VP8X"); + body.extend_from_slice(&10u32.to_le_bytes()); + body.extend_from_slice(&[0; 10]); + let exif = tiff_bytes((300, 1), (300, 1), Some(2)); + body.extend_from_slice(b"EXIF"); + body.extend_from_slice(&(exif.len() as u32).to_le_bytes()); + body.extend_from_slice(&exif); + if exif.len() % 2 == 1 { + body.push(0); + } + let riff = |size: usize, trailing: usize| { + let mut webp = b"RIFF".to_vec(); + webp.extend_from_slice(&(size as u32).to_le_bytes()); + webp.extend_from_slice(&body); + webp.extend_from_slice(&vec![0xAA; trailing]); + webp + }; + // The exact size, with or without trailing data, reads the chunk. + assert_eq!(raster_resolution(&riff(body.len(), 0)), Some((300.0, 300.0))); + assert_eq!(raster_resolution(&riff(body.len(), 4)), Some((300.0, 300.0))); + // A size declared 4 bytes short ends the container inside the EXIF chunk. + assert_eq!(raster_resolution(&riff(body.len() - 4, 4)), None); + assert_eq!(raster_resolution(&riff(body.len() - 4, 0)), None); + // A file cut inside the EXIF chunk is not read either. + let mut cut = riff(body.len(), 0); + cut.truncate(cut.len() - 6); + assert_eq!(raster_resolution(&cut), None); +} + /// A real encoder-written BMP with the header pixels-per-meter patched (compression and image /// size stay zero, as the encoder writes them). fn bmp_with_ppm(ppm: i32) -> Vec {