security: bound RAW linked-directory metadata reads

This commit is contained in:
Dylan M. Taylor
2026-10-22 07:12:17 -04:00
parent 7c78c951e4
commit 81c694a3f9
2 changed files with 29 additions and 9 deletions
+7 -6
View File
@@ -74,12 +74,13 @@ pub fn embedded_previews(bytes: &[u8]) -> Vec<Preview<'_>> {
if let (Some(off), Some(len)) = (t.tag_uint(&ifd, tag::JPEG_INTERCHANGE_FORMAT), t.tag_uint(&ifd, tag::JPEG_INTERCHANGE_FORMAT_LENGTH)) {
found.extend(candidate(bytes, off as usize, len as usize));
}
if matches!(t.tag_uint(&ifd, tag::COMPRESSION), Some(6 | 7)) {
let offs = t.tag_uints(&ifd, tag::STRIP_OFFSETS);
let lens = t.tag_uints(&ifd, tag::STRIP_BYTE_COUNTS);
if let ([off], [len]) = (offs.as_slice(), lens.as_slice()) {
found.extend(candidate(bytes, *off as usize, *len as usize));
}
if matches!(t.tag_uint(&ifd, tag::COMPRESSION), Some(6 | 7))
&& let (Some(offsets), Some(lengths)) = (ifd.get(tag::STRIP_OFFSETS), ifd.get(tag::STRIP_BYTE_COUNTS))
&& offsets.count == 1
&& lengths.count == 1
&& let (Some(off), Some(len)) = (t.uint(offsets, 0), t.uint(lengths, 0))
{
found.extend(candidate(bytes, off as usize, len as usize));
}
// Panasonic RW2 JpgFromRaw (0x002E) holds a complete JPEG.
if let Some(e) = ifd.get(0x002E).filter(|e| e.typ == 7)
+22 -3
View File
@@ -213,9 +213,11 @@ impl<'a> Tiff<'a> {
}
for t in [tag::SUB_IFDS, tag::EXIF_IFD] {
if let Some(e) = ifd.get(t) {
// Reverse so the first SubIFD is visited first.
let mut v = self.uints(e);
v.truncate(16);
// Linked-directory traversal only needs the first sixteen offsets. Read
// those values directly instead of expanding the complete file-controlled
// array and truncating it afterward.
let n = (e.count as usize).min(16);
let v = (0..n).map(|i| self.uint(e, i)).collect::<Option<Vec<_>>>().unwrap_or_default();
stack.extend(v.into_iter().rev().map(|o| o as usize));
}
}
@@ -450,3 +452,20 @@ mod tests {
assert!(out.contains("output truncated") || out.len() < 1 << 20);
}
}
#[test]
fn linked_directory_values_are_read_only_as_needed() {
// The traversal limit is independent of the TIFF entry count: callers should
// never need a complete attacker-controlled offset array to visit SubIFDs.
let mut b = b"II*\0\x08\0\0\0".to_vec();
b.extend_from_slice(&1u16.to_le_bytes());
b.extend_from_slice(&330u16.to_le_bytes());
b.extend_from_slice(&4u16.to_le_bytes());
b.extend_from_slice(&(1_000_000u32).to_le_bytes());
b.extend_from_slice(&26u32.to_le_bytes());
b.extend_from_slice(&0u32.to_le_bytes());
b.extend_from_slice(&0u32.to_le_bytes());
b.resize(26 + 16 * 4, 0);
let t = Tiff::new(&b).unwrap();
assert!(t.all_ifds().len() <= 17);
}