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]