diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hdgf/chunks/ChunkHeader.java b/poi-scratchpad/src/main/java/org/apache/poi/hdgf/chunks/ChunkHeader.java index 8f31139f535..209550a7864 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hdgf/chunks/ChunkHeader.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hdgf/chunks/ChunkHeader.java @@ -50,7 +50,11 @@ public static ChunkHeader createChunkHeader(int documentVersion, byte[] data, in ch.setType((int) LittleEndian.getUInt(data, offset)); ch.setId((int) LittleEndian.getUInt(data, offset + 4)); ch.setUnknown1((int) LittleEndian.getUInt(data, offset + 8)); - ch.setLength((int) LittleEndian.getUInt(data, offset + 12)); + // Match the v4/v5 branch below: reject lengths that would silently + // truncate when narrowing the uint32 to a signed int, rather than + // letting a wrapped value flow into the offset arithmetic in + // ChunkFactory.createChunk (offset + getLength() + sizeInBytes). + ch.setLength(Math.toIntExact(LittleEndian.getUInt(data, offset + 12))); ch.setUnknown2(LittleEndian.getShort(data, offset + 16)); ch.setUnknown3(LittleEndian.getUByte(data, offset + 18)); diff --git a/poi-scratchpad/src/test/java/org/apache/poi/hdgf/chunks/TestChunks.java b/poi-scratchpad/src/test/java/org/apache/poi/hdgf/chunks/TestChunks.java index 58254a5933c..a5a74a48537 100644 --- a/poi-scratchpad/src/test/java/org/apache/poi/hdgf/chunks/TestChunks.java +++ b/poi-scratchpad/src/test/java/org/apache/poi/hdgf/chunks/TestChunks.java @@ -20,12 +20,14 @@ Licensed to the Apache Software Foundation (ASF) under one or more import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import org.apache.poi.hdgf.chunks.ChunkFactory.CommandDefinition; import org.apache.poi.poifs.storage.RawDataUtil; +import org.apache.poi.util.LittleEndian; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -62,6 +64,50 @@ void testChunkHeaderA() { assertTrue(header.hasSeparator()); } + /** + * A v6+ chunk header reads its Length field as a 32-bit unsigned integer. + * A crafted file with Length > Integer.MAX_VALUE used to be silently + * narrowed via a plain {@code (int)} cast, letting the wrapped value flow + * into the offset arithmetic in {@code ChunkFactory.createChunk} + * ({@code offset + getLength() + sizeInBytes}). Match the v4/v5 branch, + * which has always used {@code Math.toIntExact} for its Length field, and + * reject the input up-front. + */ + @Test + void testV11RejectsOversizedLength() { + // 19-byte v6+ header: type, id, unknown1, length, unknown2 (short), unknown3 (ubyte) + byte[] header = new byte[19]; + LittleEndian.putUInt(header, 0, 0x46L); // type + LittleEndian.putUInt(header, 4, 0xFFFFFFFFL); // id (allowed -1 sentinel) + LittleEndian.putUInt(header, 8, 0x02L); // unknown1 + LittleEndian.putUInt(header, 12, 0x80000001L); // length: would wrap to a negative int + LittleEndian.putShort(header, 16, (short)0); + header[18] = 0; + + assertThrows(ArithmeticException.class, + () -> ChunkHeader.createChunkHeader(11, header, 0)); + } + + /** + * Lengths up to {@code Integer.MAX_VALUE} (still nonsensically large but + * representable) must continue to parse — the hardening is only meant to + * catch the silent-narrowing case, not to introduce a new lower ceiling. + * Downstream checks (e.g. {@code IOUtils.safelyClone}) handle bounding. + */ + @Test + void testV11AcceptsMaxIntLength() { + byte[] header = new byte[19]; + LittleEndian.putUInt(header, 0, 0x46L); + LittleEndian.putUInt(header, 4, 0x01L); + LittleEndian.putUInt(header, 8, 0x02L); + LittleEndian.putUInt(header, 12, Integer.MAX_VALUE & 0xFFFFFFFFL); + LittleEndian.putShort(header, 16, (short)0); + header[18] = 0; + + ChunkHeader h = ChunkHeader.createChunkHeader(11, header, 0); + assertEquals(Integer.MAX_VALUE, h.getLength()); + } + @Test void testChunkHeaderB() { ChunkHeader h = ChunkHeader.createChunkHeader(11, data_b, 0);