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]

Reply via email to