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 bb22350be5 boundary handling and decompression limits in CompressedRTF
(#1071)
bb22350be5 is described below
commit bb22350be5489b44c7bfe9f2640174ff4394492a
Author: jmestwa-coder <[email protected]>
AuthorDate: Tue Jun 2 04:31:25 2026 +0530
boundary handling and decompression limits in CompressedRTF (#1071)
---
.../java/org/apache/poi/hmef/CompressedRTF.java | 60 ++++++++++++++++++++--
.../org/apache/poi/hmef/TestCompressedRTF.java | 44 ++++++++++++++++
2 files changed, 99 insertions(+), 5 deletions(-)
diff --git
a/poi-scratchpad/src/main/java/org/apache/poi/hmef/CompressedRTF.java
b/poi-scratchpad/src/main/java/org/apache/poi/hmef/CompressedRTF.java
index 46988b9ec0..167d82c780 100644
--- a/poi-scratchpad/src/main/java/org/apache/poi/hmef/CompressedRTF.java
+++ b/poi-scratchpad/src/main/java/org/apache/poi/hmef/CompressedRTF.java
@@ -22,9 +22,12 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
+import org.apache.commons.io.input.BoundedInputStream;
+import org.apache.commons.io.output.ThresholdingOutputStream;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LZWDecompresser;
import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.RecordFormatException;
/**
@@ -52,6 +55,16 @@ public final class CompressedRTF extends LZWDecompresser {
"\\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New
RomanCourier" +
"{\\colortbl\\red0\\green0\\blue0\n\r\\par
\\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx";
+ private static final int DEFAULT_MAX_RECORD_LENGTH = 50_000_000;
+ /**
+ * MS-OXRTFCP uses 8-item chunks per control byte. If the data ends
mid-chunk,
+ * the remaining bits in the control byte are processed. If they are
literal bits,
+ * they produce trailing literal bytes from the stream. A chunk can thus
emit
+ * up to 7 bytes of padding beyond the declared uncompressed size.
+ */
+ private static final int MAX_PADDING_LENGTH = 7;
+ private static int MAX_RECORD_LENGTH = DEFAULT_MAX_RECORD_LENGTH;
+
private int compressedSize;
private int decompressedSize;
@@ -62,6 +75,20 @@ public final class CompressedRTF extends LZWDecompresser {
super(true, 2, true);
}
+ /**
+ * @param length the max decompressed record length allowed for
CompressedRTF
+ */
+ public static void setMaxRecordLength(int length) {
+ MAX_RECORD_LENGTH = length;
+ }
+
+ /**
+ * @return the max decompressed record length allowed for CompressedRTF
+ */
+ public static int getMaxRecordLength() {
+ return MAX_RECORD_LENGTH;
+ }
+
/**
* Decompresses the whole of the compressed RTF
* stream, outputting the resulting RTF bytes.
@@ -79,19 +106,28 @@ public final class CompressedRTF extends LZWDecompresser {
/* int dataCRC = */ LittleEndian.readInt(src);
// TODO - Handle CRC checking on the output side
+ IOUtils.safelyAllocateCheck(decompressedSize, MAX_RECORD_LENGTH);
// Do we need to do anything?
if(compressionType == UNCOMPRESSED_SIGNATURE_INT) {
// Nope, nothing fancy to do
- IOUtils.copy(src, res);
+ copyCompressedPayload(src, res);
} else if(compressionType == COMPRESSED_SIGNATURE_INT) {
- // We need to decompress it below
+ // We need to decompress it
+ int limit = decompressedSize + MAX_PADDING_LENGTH;
+ OutputStream limited = new ThresholdingOutputStream(limit,
+ stream -> { throw new RecordFormatException("Compressed RTF
expands beyond declared size " + limit); },
+ stream -> res);
+ try (InputStream bounded = BoundedInputStream.builder()
+ .setInputStream(src)
+ .setMaxCount(getCompressedSize())
+ .setPropagateClose(false)
+ .get()) {
+ super.decompress(bounded, limited);
+ }
} else {
throw new IllegalArgumentException("Invalid compression signature " +
compressionType);
}
-
- // Have it processed
- super.decompress(src, res);
}
/**
@@ -127,4 +163,18 @@ public final class CompressedRTF extends LZWDecompresser {
// Start adding new codes after the constants
return preload.length;
}
+
+ private void copyCompressedPayload(InputStream src, OutputStream res)
throws IOException {
+ long remaining = getCompressedSize();
+ byte[] buffer = IOUtils.safelyAllocate(Math.min(8192L, remaining),
MAX_RECORD_LENGTH);
+ while (remaining > 0) {
+ int read = src.read(buffer, 0, (int)Math.min(buffer.length,
remaining));
+ if (read < 0) {
+ throw new IOException("Not enough data to read " +
getCompressedSize() + " bytes of uncompressed RTF");
+ }
+ res.write(buffer, 0, read);
+ remaining -= read;
+ }
+ }
+
}
diff --git
a/poi-scratchpad/src/test/java/org/apache/poi/hmef/TestCompressedRTF.java
b/poi-scratchpad/src/test/java/org/apache/poi/hmef/TestCompressedRTF.java
index 06309dbbbe..e7cd6cf1b1 100644
--- a/poi-scratchpad/src/test/java/org/apache/poi/hmef/TestCompressedRTF.java
+++ b/poi-scratchpad/src/test/java/org/apache/poi/hmef/TestCompressedRTF.java
@@ -21,8 +21,10 @@ import static
org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -33,9 +35,12 @@ import org.apache.poi.hmef.attribute.MAPIRtfAttribute;
import org.apache.poi.hsmf.datatypes.MAPIProperty;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.RecordFormatException;
import org.apache.poi.util.StringUtil;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+@Isolated // changes static CompressedRTF.MAX_RECORD_LENGTH
public final class TestCompressedRTF {
private static final POIDataSamples _samples =
POIDataSamples.getHMEFInstance();
@@ -197,4 +202,43 @@ public final class TestCompressedRTF {
String decompStr = rtfAttr.getDataString();
assertEquals(expString, decompStr);
}
+
+ @Test
+ void testRejectsDeclaredDecompressedSizeOverLimit() throws Exception {
+ int oldLimit = CompressedRTF.getMaxRecordLength();
+ try {
+ CompressedRTF.setMaxRecordLength(4);
+
+ byte[] data = createRtfData(0, 5,
CompressedRTF.UNCOMPRESSED_SIGNATURE_INT, new byte[0]);
+ CompressedRTF comp = new CompressedRTF();
+
+ assertThrows(RecordFormatException.class, () ->
comp.decompress(new ByteArrayInputStream(data)));
+ } finally {
+ CompressedRTF.setMaxRecordLength(oldLimit);
+ }
+ }
+
+ @Test
+ void testRejectsCompressedRtfExpansionBeyondDeclaredPadding() throws
Exception {
+ ByteArrayOutputStream payload = new ByteArrayOutputStream();
+ payload.write(0xff);
+ for (int i = 0; i < 8; i++) {
+ payload.write(0);
+ payload.write(0);
+ }
+
+ byte[] data = createRtfData(payload.size(), 1,
CompressedRTF.COMPRESSED_SIGNATURE_INT, payload.toByteArray());
+ CompressedRTF comp = new CompressedRTF();
+
+ assertThrows(RecordFormatException.class, () -> comp.decompress(new
ByteArrayInputStream(data)));
+ }
+
+ private static byte[] createRtfData(int payloadLength, int
decompressedLength, int signature, byte[] payload) {
+ byte[] data = new byte[16 + payload.length];
+ LittleEndian.putInt(data, 0, payloadLength + 12);
+ LittleEndian.putInt(data, 4, decompressedLength);
+ LittleEndian.putInt(data, 8, signature);
+ System.arraycopy(payload, 0, data, 16, payload.length);
+ return data;
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]