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 0fbd33eaea fix issue with inflating picture data but the decompressed 
data is bigger than expected (#1085)
0fbd33eaea is described below

commit 0fbd33eaea9be2370c171f01dd21d09c8a3f6f33
Author: PJ Fanning <[email protected]>
AuthorDate: Mon May 25 23:29:40 2026 +0100

    fix issue with inflating picture data but the decompressed data is bigger 
than expected (#1085)
---
 .../org/apache/poi/ddf/EscherMetafileBlip.java     |   6 +-
 .../org/apache/poi/ddf/TestEscherMetafileBlip.java | 163 +++++++++++++++++++++
 2 files changed, 168 insertions(+), 1 deletion(-)

diff --git a/poi/src/main/java/org/apache/poi/ddf/EscherMetafileBlip.java 
b/poi/src/main/java/org/apache/poi/ddf/EscherMetafileBlip.java
index 339c9c0b74..caba1fdb5c 100644
--- a/poi/src/main/java/org/apache/poi/ddf/EscherMetafileBlip.java
+++ b/poi/src/main/java/org/apache/poi/ddf/EscherMetafileBlip.java
@@ -36,6 +36,7 @@ import org.apache.poi.logging.PoiLogManager;
 import org.apache.poi.hssf.usermodel.HSSFPictureData;
 import org.apache.poi.util.IOUtils;
 import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.RecordFormatException;
 
 public final class EscherMetafileBlip extends EscherBlipRecord {
     private static final Logger LOGGER = 
PoiLogManager.getLogger(EscherMetafileBlip.class);
@@ -180,7 +181,10 @@ public final class EscherMetafileBlip extends 
EscherBlipRecord {
     private static byte[] inflatePictureData(byte[] data) {
         try (InflaterInputStream in = new 
InflaterInputStream(UnsynchronizedByteArrayInputStream.builder().setByteArray(data).get());
              UnsynchronizedByteArrayOutputStream out = 
UnsynchronizedByteArrayOutputStream.builder().get()) {
-            IOUtils.copy(in, out);
+            long copied = IOUtils.copy(in, out, (long) MAX_RECORD_LENGTH + 1);
+            if (copied > MAX_RECORD_LENGTH) {
+                throw new RecordFormatException("Inflated picture data exceeds 
MAX_RECORD_LENGTH (" + MAX_RECORD_LENGTH + ")");
+            }
             return out.toByteArray();
         } catch (IOException e) {
             LOGGER.atWarn().withThrowable(e).log("Possibly corrupt compression 
or non-compressed data");
diff --git a/poi/src/test/java/org/apache/poi/ddf/TestEscherMetafileBlip.java 
b/poi/src/test/java/org/apache/poi/ddf/TestEscherMetafileBlip.java
new file mode 100644
index 0000000000..1e48327719
--- /dev/null
+++ b/poi/src/test/java/org/apache/poi/ddf/TestEscherMetafileBlip.java
@@ -0,0 +1,163 @@
+/* ====================================================================
+   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.ddf;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.zip.DeflaterOutputStream;
+
+import org.apache.poi.util.LittleEndian;
+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 for {@link EscherMetafileBlip}, including the inflation size guard
+ * against zip-bomb style payloads.
+ */
+class TestEscherMetafileBlip {
+
+    private int savedMaxRecordLength;
+
+    @BeforeEach
+    void saveLimit() {
+        savedMaxRecordLength = EscherMetafileBlip.getMaxRecordLength();
+    }
+
+    @AfterEach
+    void restoreLimit() {
+        EscherMetafileBlip.setMaxRecordLength(savedMaxRecordLength);
+    }
+
+    /**
+     * Normal case: inflated data is within the configured limit.
+     * fillFields() must succeed and return the correct uncompressed bytes.
+     */
+    @Test
+    void testInflatePictureDataWithinLimit() throws IOException {
+        byte[] plain = new byte[100];
+        Arrays.fill(plain, (byte) 'X');
+        byte[] compressed = deflate(plain);
+
+        // limit is well above 100 bytes
+        EscherMetafileBlip.setMaxRecordLength(1000);
+
+        byte[] record = buildWmfRecord(compressed, plain.length);
+        EscherMetafileBlip blip = new EscherMetafileBlip();
+        assertDoesNotThrow(() -> blip.fillFields(record, 0, new 
DefaultEscherRecordFactory()));
+        assertArrayEquals(plain, blip.getPicturedata());
+    }
+
+    /**
+     * Zip-bomb guard: inflated data exceeds MAX_RECORD_LENGTH.
+     * fillFields() must throw {@link RecordFormatException} instead of OOM.
+     */
+    @Test
+    void testInflatePictureDataExceedsLimitThrows() throws IOException {
+        // plain data is 1000 bytes; set the limit below that
+        byte[] plain = new byte[1000];
+        Arrays.fill(plain, (byte) 'A');
+        byte[] compressed = deflate(plain);
+
+        EscherMetafileBlip.setMaxRecordLength(500);
+
+        byte[] record = buildWmfRecord(compressed, plain.length);
+        EscherMetafileBlip blip = new EscherMetafileBlip();
+        RecordFormatException ex = assertThrows(RecordFormatException.class,
+                () -> blip.fillFields(record, 0, new 
DefaultEscherRecordFactory()));
+        assertTrue(ex.getMessage().contains("MAX_RECORD_LENGTH"),
+                "Exception message should mention MAX_RECORD_LENGTH");
+    }
+
+    // ---- helpers --------------------------------------------------------
+
+    /** DEFLATE-compress {@code data} using standard java.util.zip deflate. */
+    private static byte[] deflate(byte[] data) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        try (DeflaterOutputStream dos = new DeflaterOutputStream(bos)) {
+            dos.write(data);
+        }
+        return bos.toByteArray();
+    }
+
+    /**
+     * Build a minimal WMF EscherMetafileBlip record byte array from the given
+     * already-compressed picture data.
+     *
+     * Record layout (after the 8-byte Escher header):
+     *   16 bytes  UID1
+     *    4 bytes  cb (uncompressed size)
+     *   16 bytes  rcBounds (x1, y1, x2, y2 – all zero)
+     *    8 bytes  ptSize  (w, h – all zero)
+     *    4 bytes  cbSave  (compressed size)
+     *    1 byte   fCompression = 0 (DEFLATE)
+     *    1 byte   fFilter      = 0xFE
+     *    n bytes  compressed picture data
+     *
+     * Options is set to MSOBI_WMF (0x2160) so that options ^ signature == 0
+     * (not 0x10), meaning no secondary UID is written.
+     */
+    private static byte[] buildWmfRecord(byte[] compressedData, int 
uncompressedSize) {
+        // options = 0x2160 (MSOBI_WMF); recordId = 0xF01B (BLIP_WMF)
+        final short options  = 0x2160;
+        final short recordId = (short) EscherRecordTypes.BLIP_WMF.typeID;
+
+        // bytes after the 8-byte header
+        int bodyLen = 16 + 4 + 16 + 8 + 4 + 1 + 1 + compressedData.length;
+        byte[] record = new byte[8 + bodyLen];
+        int pos = 0;
+
+        // header
+        LittleEndian.putShort(record, pos, options);       pos += 2;
+        LittleEndian.putShort(record, pos, recordId);      pos += 2;
+        LittleEndian.putInt(record, pos, bodyLen);         pos += 4;
+
+        // UID1 (16 zero bytes)
+        pos += 16;
+
+        // cb (uncompressed size)
+        LittleEndian.putInt(record, pos, uncompressedSize); pos += 4;
+
+        // rcBounds (16 zero bytes)
+        pos += 16;
+
+        // ptSize (8 zero bytes)
+        pos += 8;
+
+        // cbSave (compressed size)
+        LittleEndian.putInt(record, pos, compressedData.length); pos += 4;
+
+        // fCompression = 0 (DEFLATE)
+        record[pos++] = 0;
+
+        // fFilter = 0xFE
+        record[pos++] = (byte) 0xFE;
+
+        // compressed picture data
+        System.arraycopy(compressedData, 0, record, pos, 
compressedData.length);
+
+        return record;
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to