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]

Reply via email to