This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/poi.git
The following commit(s) were added to refs/heads/trunk by this push:
new c5dedf0ebe add inflaterinputstream checks (#1086)
c5dedf0ebe is described below
commit c5dedf0ebeb044dc5b335992160fff374ca42f34
Author: PJ Fanning <[email protected]>
AuthorDate: Tue May 26 01:51:25 2026 +0100
add inflaterinputstream checks (#1086)
---
.../main/java/org/apache/poi/hslf/blip/EMF.java | 21 ++-
.../java/org/apache/poi/hslf/blip/Metafile.java | 20 +++
.../main/java/org/apache/poi/hslf/blip/PICT.java | 23 ++-
.../main/java/org/apache/poi/hslf/blip/WMF.java | 24 +++-
.../org/apache/poi/hslf/record/ExOleObjStg.java | 23 ++-
.../org/apache/poi/hwpf/usermodel/Picture.java | 33 ++++-
.../apache/poi/hslf/record/TestExOleObjStg.java | 78 ++++++++++
.../usermodel/TestMetafileInflateSizeLimit.java | 157 +++++++++++++++++++++
.../apache/poi/hwpf/usermodel/TestPictures.java | 79 +++++++++++
9 files changed, 438 insertions(+), 20 deletions(-)
diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/EMF.java
b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/EMF.java
index 7631861237..3aea2c05de 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/EMF.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/EMF.java
@@ -27,10 +27,10 @@ import
org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.poi.ddf.EscherBSERecord;
import org.apache.poi.ddf.EscherContainerRecord;
import org.apache.poi.hslf.exceptions.HSLFException;
-import org.apache.poi.hslf.record.RecordAtom;
import org.apache.poi.sl.image.ImageHeaderEMF;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.Internal;
+import org.apache.poi.util.RecordFormatException;
import org.apache.poi.util.Units;
/**
@@ -50,9 +50,15 @@ public final class EMF extends Metafile {
super(recordContainer, bse);
}
+ /**
+ * {@inheritDoc}
+ * @throws RecordFormatException if there is a problem with the size of
the decompressed data.
+ * {@link Metafile#setMaxRecordLength(int)} can be used to change the
limit applied.
+ * @throws HSLFException for parsing exceptions
+ */
@Override
- public byte[] getData(){
- byte[] rawdata = getRawData();
+ public byte[] getData() {
+ final byte[] rawdata = getRawData();
Header header = new Header();
header.read(rawdata, CHECKSUM_SIZE);
@@ -64,7 +70,12 @@ public final class EMF extends Metafile {
long len = IOUtils.skipFully(is,header.getSize() +
(long)CHECKSUM_SIZE);
assert(len == header.getSize() + CHECKSUM_SIZE);
- IOUtils.copy(inflater, out);
+ final int maxLength = getMaxRecordLength();
+ long copied = IOUtils.copy(inflater, out, (long) maxLength + 1);
+ if (copied > maxLength) {
+ throw new RecordFormatException(
+ "Inflated EMF data exceeds maximum allowed size (" +
maxLength + ")");
+ }
return out.toByteArray();
} catch (IOException e){
@@ -87,7 +98,7 @@ public final class EMF extends Metafile {
byte[] checksum = getChecksum(data);
long rawDataSize = calcRawDataSize(getUIDInstanceCount(),
checksum.length, header.getSize(), compressed.length);
- byte[] rawData = IOUtils.safelyAllocate(rawDataSize,
RecordAtom.getMaxRecordLength());
+ byte[] rawData = IOUtils.safelyAllocate(rawDataSize,
getMaxRecordLength());
int offset = 0;
System.arraycopy(checksum, 0, rawData, offset, checksum.length);
diff --git
a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/Metafile.java
b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/Metafile.java
index 98b08ff101..8c4ae1fd13 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/Metafile.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/Metafile.java
@@ -40,6 +40,26 @@ import org.apache.poi.util.Units;
*/
public abstract class Metafile extends HSLFPictureData {
+ // try to keep in synch with EscherMetafileBlip DEFAULT_MAX_RECORD_LENGTH
+ private static final int DEFAULT_MAX_RECORD_LENGTH = 100_000_000;
+ private static int MAX_RECORD_LENGTH = DEFAULT_MAX_RECORD_LENGTH;
+
+ /**
+ * @param length the max record length allowed for HWPF Picture
+ * @since 6.0.0
+ */
+ public static void setMaxRecordLength(int length) {
+ MAX_RECORD_LENGTH = length;
+ }
+
+ /**
+ * @return the max record length allowed for HWPF Picture
+ * @since 6.0.0
+ */
+ public static int getMaxRecordLength() {
+ return MAX_RECORD_LENGTH;
+ }
+
/**
* Creates a new instance.
*
diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/PICT.java
b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/PICT.java
index 75a8d7f93b..f7ee779e60 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/PICT.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/PICT.java
@@ -33,10 +33,10 @@ import org.apache.poi.logging.PoiLogManager;
import org.apache.poi.ddf.EscherBSERecord;
import org.apache.poi.ddf.EscherContainerRecord;
import org.apache.poi.hslf.exceptions.HSLFException;
-import org.apache.poi.hslf.record.RecordAtom;
import org.apache.poi.sl.image.ImageHeaderPICT;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.Internal;
+import org.apache.poi.util.RecordFormatException;
import org.apache.poi.util.Units;
/**
@@ -57,13 +57,19 @@ public final class PICT extends Metafile {
super(recordContainer, bse);
}
+ /**
+ * {@inheritDoc}
+ * @throws RecordFormatException if there is a problem with the size of
the decompressed data.
+ * {@link Metafile#setMaxRecordLength(int)} can be used to change the
limit applied.
+ * @throws HSLFException for parsing exceptions
+ */
@Override
- public byte[] getData(){
+ public byte[] getData() {
byte[] rawdata = getRawData();
try (UnsynchronizedByteArrayOutputStream out =
UnsynchronizedByteArrayOutputStream.builder().get()) {
byte[] macheader = new byte[512];
out.write(macheader);
- int pos = CHECKSUM_SIZE*getUIDInstanceCount();
+ int pos = Math.multiplyExact(CHECKSUM_SIZE, getUIDInstanceCount());
byte[] pict = read(rawdata, pos);
out.write(pict);
return out.toByteArray();
@@ -83,14 +89,23 @@ public final class PICT extends Metafile {
}
byte[] chunk = new byte[4096];
try (UnsynchronizedByteArrayOutputStream out =
UnsynchronizedByteArrayOutputStream.builder().setBufferSize(header.getWmfSize()).get())
{
+ final int maxLength = getMaxRecordLength();
+ long totalInflated = 0;
try (InflaterInputStream inflater = new
InflaterInputStream(bis)) {
int count;
while ((count = inflater.read(chunk)) >= 0) {
+ totalInflated += count;
+ if (totalInflated > maxLength) {
+ throw new RecordFormatException(
+ "Inflated PICT data exceeds maximum
allowed size (" + maxLength + ")");
+ }
out.write(chunk, 0, count);
// PICT zip-stream can be erroneous, so we clear the
array to determine
// the maximum of read bytes, after the inflater
crashed
Arrays.fill(chunk, (byte) 0);
}
+ } catch (RecordFormatException e) {
+ throw e;
} catch (Exception e) {
int lastLen = chunk.length - 1;
while (lastLen >= 0 && chunk[lastLen] == 0) {
@@ -129,7 +144,7 @@ public final class PICT extends Metafile {
byte[] checksum = getChecksum(data);
long rawDataSize = calcRawDataSize(getUIDInstanceCount(),
checksum.length, header.getSize(), compressed.length);
- byte[] rawData = IOUtils.safelyAllocate(rawDataSize,
RecordAtom.getMaxRecordLength());
+ byte[] rawData = IOUtils.safelyAllocate(rawDataSize,
getMaxRecordLength());
int offset = 0;
System.arraycopy(checksum, 0, rawData, offset, checksum.length);
diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/WMF.java
b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/WMF.java
index d09b9d3964..df05410385 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/WMF.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/blip/WMF.java
@@ -27,10 +27,10 @@ import
org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.poi.ddf.EscherBSERecord;
import org.apache.poi.ddf.EscherContainerRecord;
import org.apache.poi.hslf.exceptions.HSLFException;
-import org.apache.poi.hslf.record.RecordAtom;
import org.apache.poi.sl.image.ImageHeaderWMF;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.Internal;
+import org.apache.poi.util.RecordFormatException;
import org.apache.poi.util.Units;
/**
@@ -50,14 +50,19 @@ public final class WMF extends Metafile {
super(recordContainer, bse);
}
+ /**
+ * {@inheritDoc}
+ * @throws RecordFormatException if there is a problem with the size of
the decompressed data.
+ * {@link Metafile#setMaxRecordLength(int)} can be used to change the
limit applied.
+ * @throws HSLFException for parsing exceptions
+ */
@Override
- public byte[] getData(){
- byte[] rawdata = getRawData();
+ public byte[] getData() {
+ final byte[] rawdata = getRawData();
try (InputStream is =
UnsynchronizedByteArrayInputStream.builder().setByteArray(rawdata).get()) {
-
Header header = new Header();
- header.read(rawdata, CHECKSUM_SIZE*getUIDInstanceCount());
+ header.read(rawdata, Math.multiplyExact(CHECKSUM_SIZE,
getUIDInstanceCount()));
long skipLen = header.getSize() +
(long)CHECKSUM_SIZE*getUIDInstanceCount();
long skipped = IOUtils.skipFully(is, skipLen);
assert(skipped == skipLen);
@@ -67,7 +72,12 @@ public final class WMF extends Metafile {
aldus.write(out);
try (InflaterInputStream inflater = new InflaterInputStream( is ))
{
- IOUtils.copy(inflater, out);
+ final int maxLength = getMaxRecordLength();
+ long copied = IOUtils.copy(inflater, out, (long) maxLength +
1);
+ if (copied > maxLength) {
+ throw new RecordFormatException(
+ "Inflated WMF data exceeds maximum allowed size ("
+ maxLength + ")");
+ }
}
return out.toByteArray();
} catch (IOException e){
@@ -92,7 +102,7 @@ public final class WMF extends Metafile {
byte[] checksum = getChecksum(data);
long rawDataSize = calcRawDataSize(getUIDInstanceCount(),
checksum.length, header.getSize(), compressed.length);
- byte[] rawData = IOUtils.safelyAllocate(rawDataSize,
RecordAtom.getMaxRecordLength());
+ byte[] rawData = IOUtils.safelyAllocate(rawDataSize,
getMaxRecordLength());
int offset = 0;
System.arraycopy(checksum, 0, rawData, offset, checksum.length);
diff --git
a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/ExOleObjStg.java
b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/ExOleObjStg.java
index dfff540574..07818a081f 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/ExOleObjStg.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/record/ExOleObjStg.java
@@ -33,6 +33,7 @@ import
org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.poi.util.GenericRecordUtil;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.RecordFormatException;
/**
* Storage for embedded OLE objects.
@@ -40,7 +41,22 @@ import org.apache.poi.util.LittleEndian;
public class ExOleObjStg extends PositionDependentRecordAtom implements
PersistRecord {
//arbitrarily selected; may need to increase
- private static final int MAX_RECORD_LENGTH = 100_000_000;
+ private static final int DEFAULT_MAX_RECORD_LENGTH = 100_000_000;
+ private static int MAX_RECORD_LENGTH = DEFAULT_MAX_RECORD_LENGTH;
+
+ /**
+ * @param length the max record length allowed for ExOleObjStg
+ */
+ public static void setMaxRecordLength(int length) {
+ MAX_RECORD_LENGTH = length;
+ }
+
+ /**
+ * @return the max record length allowed for ExOleObjStg
+ */
+ public static int getMaxRecordLength() {
+ return MAX_RECORD_LENGTH;
+ }
private int _persistId; // Found from PersistPtrHolder
@@ -103,11 +119,16 @@ public class ExOleObjStg extends
PositionDependentRecordAtom implements PersistR
* Opens an input stream which will decompress the data on the fly.
*
* @return the data input stream.
+ * @throws RecordFormatException if the claimed uncompressed size exceeds
the maximum allowed.
* @throws UncheckedIOException if the data size exceeds the expected size.
*/
public InputStream getData() {
if (isCompressed()) {
int size = LittleEndian.getInt(_data);
+ if (size < 0 || size > MAX_RECORD_LENGTH) {
+ throw new RecordFormatException(
+ "Claimed uncompressed data size (" + size + ") exceeds
maximum allowed (" + MAX_RECORD_LENGTH + ")");
+ }
InputStream compressedStream = new ByteArrayInputStream(_data, 4,
_data.length);
try {
diff --git
a/poi-scratchpad/src/main/java/org/apache/poi/hwpf/usermodel/Picture.java
b/poi-scratchpad/src/main/java/org/apache/poi/hwpf/usermodel/Picture.java
index 77c695c3d0..0713cf574b 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hwpf/usermodel/Picture.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hwpf/usermodel/Picture.java
@@ -41,6 +41,7 @@ import org.apache.poi.hwpf.model.PICFAndOfficeArtData;
import org.apache.poi.logging.PoiLogManager;
import org.apache.poi.sl.image.ImageHeaderPNG;
import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.RecordFormatException;
import org.apache.poi.util.StringUtil;
import org.apache.poi.util.Units;
@@ -88,6 +89,26 @@ public final class Picture {
return matched;
}
+ // try to keep in synch with EscherMetafileBlip DEFAULT_MAX_RECORD_LENGTH
+ private static final int DEFAULT_MAX_RECORD_LENGTH = 100_000_000;
+ private static int MAX_RECORD_LENGTH = DEFAULT_MAX_RECORD_LENGTH;
+
+ /**
+ * @param length the max record length allowed for HWPF Picture
+ * @since 6.0.0
+ */
+ public static void setMaxRecordLength(int length) {
+ MAX_RECORD_LENGTH = length;
+ }
+
+ /**
+ * @return the max record length allowed for HWPF Picture
+ * @since 6.0.0
+ */
+ public static int getMaxRecordLength() {
+ return MAX_RECORD_LENGTH;
+ }
+
private PICF _picf;
private PICFAndOfficeArtData _picfAndOfficeArtData;
private final List<? extends EscherRecord> _blipRecords;
@@ -109,8 +130,9 @@ public final class Picture {
}
/**
- * Builds a Picture object for a Picture stored in the
- * DataStream
+ * Builds a Picture object for a Picture stored in the DataStream
+ * @throws RecordFormatException if there is a problem with the size of
the decompressed data.
+ * {@link #setMaxRecordLength(int)} can be used to change the limit
applied.
*/
public Picture( int dataBlockStartOfsset, byte[] _dataStream, boolean
fillBytes ) { // NOSONAR
_picfAndOfficeArtData = new PICFAndOfficeArtData( _dataStream,
dataBlockStartOfsset );
@@ -145,7 +167,12 @@ public final class Picture {
InflaterInputStream in = new InflaterInputStream(bis);
UnsynchronizedByteArrayOutputStream out =
UnsynchronizedByteArrayOutputStream.builder().get()) {
- IOUtils.copy(in, out);
+ final int maxSize = getMaxRecordLength();
+ long copied = IOUtils.copy(in, out, (long) maxSize + 1);
+ if (copied > maxSize) {
+ throw new RecordFormatException(
+ "Inflated picture data exceeds maximum allowed
size (" + maxSize + ")");
+ }
content = out.toByteArray();
} catch (IOException e) {
/*
diff --git
a/poi-scratchpad/src/test/java/org/apache/poi/hslf/record/TestExOleObjStg.java
b/poi-scratchpad/src/test/java/org/apache/poi/hslf/record/TestExOleObjStg.java
index 8563bf7c5e..0ab526f969 100644
---
a/poi-scratchpad/src/test/java/org/apache/poi/hslf/record/TestExOleObjStg.java
+++
b/poi-scratchpad/src/test/java/org/apache/poi/hslf/record/TestExOleObjStg.java
@@ -21,6 +21,7 @@ package org.apache.poi.hslf.record;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
@@ -28,7 +29,11 @@ import
org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream;
import org.apache.poi.poifs.filesystem.DocumentEntry;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.RecordFormatException;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
@@ -40,6 +45,18 @@ public final class TestExOleObjStg {
// <ExOleObjStg info="16" type="4113" size="347" offset="4322" header="10
00 11 10 5B 01 00 00 ">....
private static byte[] data;
+ private int savedMaxRecordLength;
+
+ @BeforeEach
+ void saveMaxRecordLength() {
+ savedMaxRecordLength = ExOleObjStg.getMaxRecordLength();
+ }
+
+ @AfterEach
+ void restoreMaxRecordLength() {
+ ExOleObjStg.setMaxRecordLength(savedMaxRecordLength);
+ }
+
@BeforeAll
public static void init() throws IOException {
data = org.apache.poi.poifs.storage.RawDataUtil.decompress(
@@ -95,4 +112,65 @@ public final class TestExOleObjStg {
assertEquals(data.length, b.length);
assertArrayEquals(data, b);
}
+
+ @Test
+ void testGetDataRejectsOversizedClaimedSize() {
+ // Build a minimal compressed ExOleObjStg record whose claimed
+ // uncompressed size (first 4 bytes of _data) exceeds
MAX_RECORD_LENGTH.
+ // header: 8 bytes, with bytes [0..1] != 0 to make isCompressed()
return true
+ byte[] header = new byte[8];
+ LittleEndian.putShort(header, 0, (short) 0x10); // non-zero ->
compressed
+
+ // _data: first 4 bytes = claimed size (200_000_001 > default
100_000_000),
+ // followed by a trivial DEFLATE stream (1 empty stored block)
+ byte[] deflateEmpty = { 0x78, (byte) 0x9C, 0x03, 0x00, 0x00, 0x00,
0x00, 0x01 };
+ byte[] recordData = new byte[4 + deflateEmpty.length];
+ LittleEndian.putInt(recordData, 0, 200_000_001);
+ System.arraycopy(deflateEmpty, 0, recordData, 4, deflateEmpty.length);
+
+ byte[] rawRecord = new byte[8 + recordData.length];
+ System.arraycopy(header, 0, rawRecord, 0, 8);
+ System.arraycopy(recordData, 0, rawRecord, 8, recordData.length);
+
+ ExOleObjStg record = new ExOleObjStg(rawRecord, 0, rawRecord.length);
+ assertThrows(RecordFormatException.class, record::getData,
+ "getData() must throw RecordFormatException when claimed size
exceeds MAX_RECORD_LENGTH");
+ }
+
+ @Test
+ void testGetDataRejectsNegativeClaimedSize() {
+ // A claimed size stored as a negative integer (sign bit set) must be
rejected.
+ byte[] header = new byte[8];
+ LittleEndian.putShort(header, 0, (short) 0x10);
+
+ byte[] recordData = new byte[8];
+ LittleEndian.putInt(recordData, 0, -1); // negative size
+
+ byte[] rawRecord = new byte[8 + recordData.length];
+ System.arraycopy(header, 0, rawRecord, 0, 8);
+ System.arraycopy(recordData, 0, rawRecord, 8, recordData.length);
+
+ ExOleObjStg record = new ExOleObjStg(rawRecord, 0, rawRecord.length);
+ assertThrows(RecordFormatException.class, record::getData,
+ "getData() must throw RecordFormatException when claimed size
is negative");
+ }
+
+ @Test
+ void testGetDataWithinLimit() throws Exception {
+ // The existing real-file data has a small decompressed size; this
must work normally.
+ ExOleObjStg record = new ExOleObjStg(data, 0, data.length);
+ assertNotNull(record.getData(),
+ "getData() must succeed when the claimed size is within
MAX_RECORD_LENGTH");
+ }
+
+ @Test
+ void testGetDataRejectsWhenLimitLowered() throws Exception {
+ // Lower MAX_RECORD_LENGTH below the record's actual decompressed size,
+ // then verify that getData() throws.
+ ExOleObjStg record = new ExOleObjStg(data, 0, data.length);
+ int actualSize = record.getDataLength(); // e.g. ~23_000 bytes for the
test record
+ ExOleObjStg.setMaxRecordLength(actualSize - 1);
+ assertThrows(RecordFormatException.class, record::getData,
+ "getData() must throw when the claimed size exceeds the
lowered MAX_RECORD_LENGTH");
+ }
}
diff --git
a/poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestMetafileInflateSizeLimit.java
b/poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestMetafileInflateSizeLimit.java
new file mode 100644
index 0000000000..63de4660dc
--- /dev/null
+++
b/poi-scratchpad/src/test/java/org/apache/poi/hslf/usermodel/TestMetafileInflateSizeLimit.java
@@ -0,0 +1,157 @@
+/* ====================================================================
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+==================================================================== */
+
+package org.apache.poi.hslf.usermodel;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.poi.POIDataSamples;
+import org.apache.poi.hslf.blip.EMF;
+import org.apache.poi.hslf.blip.Metafile;
+import org.apache.poi.hslf.blip.PICT;
+import org.apache.poi.hslf.blip.WMF;
+import org.apache.poi.sl.usermodel.PictureData.PictureType;
+import org.apache.poi.util.RecordFormatException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that WMF, EMF, and PICT inflate operations respect the
+ * {@link Metafile#getMaxRecordLength()} size limit to prevent zip-bomb
+ * style decompression attacks.
+ */
+public class TestMetafileInflateSizeLimit {
+
+ private static final POIDataSamples SLIDE_TESTS =
POIDataSamples.getSlideShowInstance();
+
+ private int savedMaxRecordLength;
+
+ @BeforeEach
+ void saveLimit() {
+ savedMaxRecordLength = Metafile.getMaxRecordLength();
+ }
+
+ @AfterEach
+ void restoreLimit() {
+ Metafile.setMaxRecordLength(savedMaxRecordLength);
+ }
+
+ // -----------------------------------------------------------------------
+ // WMF
+ // -----------------------------------------------------------------------
+
+ @Test
+ void testWmfGetDataWithinLimit() throws IOException {
+ byte[] wmfBytes = SLIDE_TESTS.readFile("santa.wmf");
+ try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+ HSLFPictureData pd = ppt.addPicture(wmfBytes, PictureType.WMF);
+ assertDoesNotThrow(pd::getData,
+ "WMF getData() should succeed when limit is not exceeded");
+ }
+ }
+
+ @Test
+ void testWmfGetDataExceedsLimitThrows() throws IOException {
+ byte[] wmfBytes = SLIDE_TESTS.readFile("santa.wmf");
+ try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+ HSLFPictureData pd = ppt.addPicture(wmfBytes, PictureType.WMF);
+ // Set limit far below the actual decompressed size
+ Metafile.setMaxRecordLength(10);
+ assertThrows(RecordFormatException.class, pd::getData,
+ "WMF getData() should throw RecordFormatException when
limit is exceeded");
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // EMF
+ // -----------------------------------------------------------------------
+
+ @Test
+ void testEmfGetDataWithinLimit() throws IOException {
+ byte[] emfBytes = SLIDE_TESTS.readFile("wrench.emf");
+ try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+ HSLFPictureData pd = ppt.addPicture(emfBytes, PictureType.EMF);
+ assertDoesNotThrow(pd::getData,
+ "EMF getData() should succeed when limit is not exceeded");
+ }
+ }
+
+ @Test
+ void testEmfGetDataExceedsLimitThrows() throws IOException {
+ byte[] emfBytes = SLIDE_TESTS.readFile("wrench.emf");
+ try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+ HSLFPictureData pd = ppt.addPicture(emfBytes, PictureType.EMF);
+ // Set limit far below the actual decompressed size
+ Metafile.setMaxRecordLength(10);
+ assertThrows(RecordFormatException.class, pd::getData,
+ "EMF getData() should throw RecordFormatException when
limit is exceeded");
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // PICT
+ // -----------------------------------------------------------------------
+
+ @Test
+ void testPictGetDataWithinLimit() throws IOException {
+ byte[] pictBytes = SLIDE_TESTS.readFile("cow.pict");
+ try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+ HSLFPictureData pd = ppt.addPicture(pictBytes, PictureType.PICT);
+ assertDoesNotThrow(pd::getData,
+ "PICT getData() should succeed when limit is not
exceeded");
+ }
+ }
+
+ @Test
+ void testPictGetDataExceedsLimitThrows() throws IOException {
+ byte[] pictBytes = SLIDE_TESTS.readFile("cow.pict");
+ try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+ HSLFPictureData pd = ppt.addPicture(pictBytes, PictureType.PICT);
+ // Set limit far below the actual decompressed size
+ Metafile.setMaxRecordLength(10);
+ assertThrows(RecordFormatException.class, pd::getData,
+ "PICT getData() should throw RecordFormatException when
limit is exceeded");
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Verify types
+ // -----------------------------------------------------------------------
+
+ @Test
+ void testWmfAndEmfAndPictPictureTypes() throws IOException {
+ byte[] wmfBytes = SLIDE_TESTS.readFile("santa.wmf");
+ byte[] emfBytes = SLIDE_TESTS.readFile("wrench.emf");
+ byte[] pictBytes = SLIDE_TESTS.readFile("cow.pict");
+ try (HSLFSlideShow ppt = new HSLFSlideShow()) {
+ HSLFPictureData wmfPd = ppt.addPicture(wmfBytes, PictureType.WMF);
+ HSLFPictureData emfPd = ppt.addPicture(emfBytes, PictureType.EMF);
+ HSLFPictureData pictPd = ppt.addPicture(pictBytes,
PictureType.PICT);
+
+ List<HSLFPictureData> allPics = ppt.getPictureData();
+ assertInstanceOf(WMF.class, allPics.get(0));
+ assertInstanceOf(EMF.class, allPics.get(1));
+ assertInstanceOf(PICT.class, allPics.get(2));
+ }
+ }
+}
diff --git
a/poi-scratchpad/src/test/java/org/apache/poi/hwpf/usermodel/TestPictures.java
b/poi-scratchpad/src/test/java/org/apache/poi/hwpf/usermodel/TestPictures.java
index 96caa82a24..cf126133be 100644
---
a/poi-scratchpad/src/test/java/org/apache/poi/hwpf/usermodel/TestPictures.java
+++
b/poi-scratchpad/src/test/java/org/apache/poi/hwpf/usermodel/TestPictures.java
@@ -22,17 +22,25 @@ import static
org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.util.Arrays;
import java.util.List;
+import java.util.zip.DeflaterOutputStream;
import org.apache.poi.POIDataSamples;
+import org.apache.poi.ddf.EscherBlipRecord;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.HWPFTestDataSamples;
import org.apache.poi.hwpf.model.PicturesTable;
+import org.apache.poi.util.RecordFormatException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -43,6 +51,18 @@ import org.junit.jupiter.params.provider.ValueSource;
*/
public final class TestPictures {
+ private int savedMaxRecordLength;
+
+ @BeforeEach
+ void saveMaxRecordLength() {
+ savedMaxRecordLength = Picture.getMaxRecordLength();
+ }
+
+ @AfterEach
+ void restoreMaxRecordLength() {
+ Picture.setMaxRecordLength(savedMaxRecordLength);
+ }
+
/**
* two jpegs
*/
@@ -374,4 +394,63 @@ public final class TestPictures {
List<Picture> picturesA = picA.getAllPictures();
assertEquals(expectedCount, picturesA.size());
}
+
+ // -----------------------------------------------------------------------
+ // Inflate size-limit tests for compressed pictures
+ // -----------------------------------------------------------------------
+
+ /**
+ * Builds raw picture content in the HWPF compressed-picture format:
+ * - 32 bytes of padding
+ * - byte 32 = 0xFE (COMPRESSED2[0])
+ * - bytes 33+: zlib/deflate stream starting with 0x78 0x9C
+ *
+ * This matches the COMPRESSED2 signature checked in {@link
Picture#fillImageContent()}.
+ */
+ private static byte[] buildCompressedPictureRaw(byte[] plainData) throws
IOException {
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try (DeflaterOutputStream dos = new DeflaterOutputStream(bos)) {
+ dos.write(plainData);
+ }
+ byte[] deflated = bos.toByteArray();
+ // deflated[0] == 0x78, deflated[1] == 0x9C for default compression
level
+
+ byte[] raw = new byte[32 + 1 + deflated.length];
+ // bytes 0..31: padding (zeros)
+ raw[32] = (byte) 0xFE;
+ System.arraycopy(deflated, 0, raw, 33, deflated.length);
+ return raw;
+ }
+
+ @Test
+ void testCompressedPictureGetContentWithinLimit() throws IOException {
+ byte[] plain = new byte[100];
+ Arrays.fill(plain, (byte) 'X');
+ byte[] raw = buildCompressedPictureRaw(plain);
+
+ EscherBlipRecord blip = new EscherBlipRecord();
+ blip.setPictureData(raw);
+ Picture picture = new Picture(blip);
+
+ // Limit is well above 100 bytes; getContent() must succeed
+ Picture.setMaxRecordLength(10_000);
+ byte[] content = picture.getContent();
+ assertNotNull(content);
+ }
+
+ @Test
+ void testCompressedPictureGetContentExceedsLimitThrows() throws
IOException {
+ byte[] plain = new byte[1000];
+ Arrays.fill(plain, (byte) 'A');
+ byte[] raw = buildCompressedPictureRaw(plain);
+
+ EscherBlipRecord blip = new EscherBlipRecord();
+ blip.setPictureData(raw);
+ Picture picture = new Picture(blip);
+
+ // Set the limit below the actual decompressed size
+ Picture.setMaxRecordLength(500);
+ assertThrows(RecordFormatException.class, picture::getContent,
+ "getContent() must throw RecordFormatException when inflated
data exceeds the limit");
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]