diff --git a/crates/zip/src/lib.rs b/crates/zip/src/lib.rs index 77ad30cc8..d61968fbb 100644 --- a/crates/zip/src/lib.rs +++ b/crates/zip/src/lib.rs @@ -604,6 +604,14 @@ fn write_small_zip_entry(reader: &mut R, output_path: &Path, size: u64) let size = usize::try_from(size).map_err(|_| io::Error::other("small zip entry size overflow"))?; let mut buffer = [0_u8; SMALL_ZIP_EXTRACT_FAST_PATH_LIMIT as usize]; reader.read_exact(&mut buffer[..size])?; + // `read_exact` stops as soon as the declared bytes are read and never performs the + // terminal zero-length read that the zip crate's `Crc32Reader` uses to validate the + // entry checksum. Force one extra read to EOF so a corrupted small entry is rejected + // here, matching the large-entry `io::copy` path which already reads through EOF. + let mut trailing = [0_u8; 1]; + if reader.read(&mut trailing)? != 0 { + return Err(io::Error::other("small zip entry produced more data than its declared size").into()); + } std::fs::write(output_path, &buffer[..size])?; Ok(()) } @@ -1557,6 +1565,39 @@ mod tests { ); } + #[tokio::test] + async fn test_extract_zip_rejects_small_entry_with_corrupted_crc() { + let temp = tempdir().expect("tempdir should be created"); + let zip_path = temp.path().join("corrupt-crc.zip"); + let extract_path = temp.path().join("extract"); + + // A stored entry well under the small-entry fast-path limit so extraction takes + // the in-memory buffer path rather than the streaming `io::copy` path. + let content = b"small-entry-crc-payload"; + assert!((content.len() as u64) <= SMALL_ZIP_EXTRACT_FAST_PATH_LIMIT); + build_zip_file_with_entries(&zip_path, &[("small.txt", content)]) + .await + .expect("zip fixture should be created"); + + // Corrupt the stored entry data so its bytes no longer match the recorded CRC32. + let mut raw = fs::read(&zip_path).await.expect("zip fixture should be readable"); + let offset = raw + .windows(content.len()) + .position(|window| window == content) + .expect("stored entry data should be present in the zip"); + raw[offset] ^= 0xFF; + fs::write(&zip_path, &raw).await.expect("corrupted zip should be writable"); + + let err = extract_zip_simple(&zip_path, &extract_path) + .await + .expect_err("small entry with a corrupted CRC should be rejected"); + + assert!( + matches!(err, ZipError::Io(ref e) if e.kind() == std::io::ErrorKind::InvalidData), + "expected an InvalidData checksum error, got {err:?}" + ); + } + #[tokio::test] async fn test_extract_zip_allows_entry_count_exactly_at_limit() { let temp = tempdir().expect("tempdir should be created");