This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch 2.0.X
in repository https://gitbox.apache.org/repos/asf/mina.git

commit 30052eae86b672a52cc6ef89dd308612625654c0
Author: Emmanuel Lécharny <[email protected]>
AuthorDate: Sun May 24 11:59:13 2026 +0200

    Backported fix for compression filter
---
 .../mina/filter/compression/CompressionFilter.java |  43 +++++-
 .../org/apache/mina/filter/compression/Zlib.java   | 102 +++++++------
 .../apache/mina/filter/compression/ZlibTest.java   | 157 +++++++++++++++++----
 3 files changed, 231 insertions(+), 71 deletions(-)

diff --git 
a/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/CompressionFilter.java
 
b/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/CompressionFilter.java
index b2260ec84..e6b823e6f 100644
--- 
a/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/CompressionFilter.java
+++ 
b/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/CompressionFilter.java
@@ -108,12 +108,18 @@ public class CompressionFilter extends WriteRequestFilter 
{
     /** The maximum decompressed size, to avoid an OOM. Default to 1Mb */
     private int maxDecompressedSize;
 
+    /** Maximum decompression ratio **/
+    private final long maxDecompressRatio;
+
+    /** Grace size before decompression ratio check is enforced **/
+    private final long decompressRatioMinSize;
+
     /**
      * Creates a new instance which compresses outboud data and decompresses
      * inbound data with default compression level.
      */
     public CompressionFilter() {
-        this(true, true, COMPRESSION_DEFAULT, Zlib.MAX_DECOMPRESSED_SIZE);
+        this(true, true, COMPRESSION_DEFAULT, Zlib.MAX_DECOMPRESSED_SIZE, 
Zlib.MAX_DECOMPRESS_RATIO, Zlib.DECOMPRESS_RATIO_MIN_SIZE);
     }
 
     /**
@@ -127,7 +133,7 @@ public class CompressionFilter extends WriteRequestFilter {
      *                         {@link #COMPRESSION_NONE}.
      */
     public CompressionFilter(final int compressionLevel) {
-        this(true, true, compressionLevel, Zlib.MAX_DECOMPRESSED_SIZE);
+        this(true, true, compressionLevel, Zlib.MAX_DECOMPRESSED_SIZE, 
Zlib.MAX_DECOMPRESS_RATIO, Zlib.DECOMPRESS_RATIO_MIN_SIZE);
     }
 
     /**
@@ -143,7 +149,7 @@ public class CompressionFilter extends WriteRequestFilter {
      */
     public CompressionFilter(final boolean compressInbound, final boolean 
compressOutbound, 
             final int compressionLevel) {
-        this(true, true, compressionLevel, Zlib.MAX_DECOMPRESSED_SIZE);
+        this(compressInbound, compressOutbound, compressionLevel, 
Zlib.MAX_DECOMPRESSED_SIZE, Zlib.MAX_DECOMPRESS_RATIO, 
Zlib.DECOMPRESS_RATIO_MIN_SIZE);
     }
 
     /**
@@ -159,13 +165,40 @@ public class CompressionFilter extends WriteRequestFilter 
{
      *                         {@link #COMPRESSION_MIN}, and
      *                         {@link #COMPRESSION_NONE}.
      * @param maxDecompressedSize The maximum size for a buffer when inflating 
some data
+     * @since 2.2.8
      */
     public CompressionFilter(final boolean compressInbound, final boolean 
compressOutbound, 
             final int compressionLevel, final int maxDecompressedSize) {
+        this(compressInbound, compressOutbound, compressionLevel, 
maxDecompressedSize, Zlib.MAX_DECOMPRESS_RATIO, Zlib.DECOMPRESS_RATIO_MIN_SIZE);
+    }
+
+    /**
+     * Creates a new instance with explicit zip-bomb protection parameters.
+     *
+     * @param compressInbound <code>true</code> if data read is to be 
decompressed
+     * @param compressOutbound <code>true</code> if data written is to be 
compressed
+     * @param compressionLevel the level of compression to be used. Must
+     *                         be one of {@link #COMPRESSION_DEFAULT},
+     *                         {@link #COMPRESSION_MAX},
+     *                         {@link #COMPRESSION_MIN}, and
+     *                         {@link #COMPRESSION_NONE}.
+     * @param maxDecompressedSize the maximum size for a buffer when inflating 
data
+     * @param maxDecompressRatio the maximum allowed cumulative ratio of
+     *                           decompressed to compressed bytes.
+     *                           A value &lt;= 0 disables the check.
+     * @param decompressRatioMinSize the minimum cumulative decompressed size
+     *                               below which the ratio check is skipped.
+     * @since 2.2.8
+     */
+    public CompressionFilter(final boolean compressInbound, final boolean 
compressOutbound,
+            final int compressionLevel, final int maxDecompressedSize,
+            final long maxDecompressRatio, final long decompressRatioMinSize) {
         this.compressionLevel = compressionLevel;
         this.compressInbound = compressInbound;
         this.compressOutbound = compressOutbound;
         this.maxDecompressedSize = maxDecompressedSize;
+        this.maxDecompressRatio = maxDecompressRatio;
+        this.decompressRatioMinSize = decompressRatioMinSize;
     }
 
     @Override
@@ -224,8 +257,8 @@ public class CompressionFilter extends WriteRequestFilter {
             throw new IllegalStateException("Only one " + 
CompressionFilter.class + " is permitted.");
         }
 
-        Zlib deflater = new Zlib(compressionLevel, Zlib.MODE_DEFLATER);
-        Zlib inflater = new Zlib(compressionLevel, Zlib.MODE_INFLATER, 
maxDecompressedSize);
+        Zlib deflater = new Zlib(compressionLevel, Zlib.MODE_INFLATER, 
maxDecompressedSize, maxDecompressRatio, decompressRatioMinSize);
+        Zlib inflater = new Zlib(compressionLevel, Zlib.MODE_INFLATER, 
maxDecompressedSize, maxDecompressRatio, decompressRatioMinSize);
 
         IoSession session = parent.getSession();
 
diff --git 
a/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/Zlib.java
 
b/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/Zlib.java
index c84712b12..ad72be5de 100644
--- 
a/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/Zlib.java
+++ 
b/mina-filter-compression/src/main/java/org/apache/mina/filter/compression/Zlib.java
@@ -29,7 +29,7 @@ import com.jcraft.jzlib.ZStream;
 /**
  * A helper class for interfacing with the JZlib library. This class acts both
  * as a compressor and decompressor, but only as one at a time.  The only
- * flush method supported is {@code Z_SYNC_FLUSH} also known as {@code 
Z_PARTIAL_FLUSH}
+ * flush method supported is <code>Z_SYNC_FLUSH</code> also known as 
<code>Z_PARTIAL_FLUSH</code>
  *
  * @author <a href="http://mina.apache.org";>Apache MINA Project</a>
  */
@@ -54,13 +54,32 @@ class Zlib {
 
     /** The requested compression level */
     private int compressionLevel;
-    
+
     /** The maximum size of an inflated buffer. Default to 1Mb */
-    /* Package protected */ 
+    /* Package protected */
     static final int MAX_DECOMPRESSED_SIZE = Integer.MAX_VALUE;
 
+    /**
+     * Default maximum decompression ratio (decompressed / compressed).
+     */
+    /* Package protected */
+    static final long MAX_DECOMPRESS_RATIO = 100L;
+
+    /**
+     * Grace size before decompression ratio check is enforced.
+     *
+     * <p>Below this threshold the check is skipped to avoid false positives 
on small payloads where framing/header
+     * overhead dominates the ratio.</p>
+     */
+    /* Package protected */
+    static final long DECOMPRESS_RATIO_MIN_SIZE = 1024L * 1024L;
+
     private int maxDecompressedSize = MAX_DECOMPRESSED_SIZE;
 
+    private long maxDecompressRatio = MAX_DECOMPRESS_RATIO;
+
+    private long decompressRatioMinSize = DECOMPRESS_RATIO_MIN_SIZE;
+
     /** The inner stream used to inflate or deflate the data */
     private ZStream zStream = null;
 
@@ -71,54 +90,35 @@ class Zlib {
      * Creates an instance of the ZLib class.
      * 
      * @param compressionLevel the level of compression that should be used. 
One of
-     * {@code COMPRESSION_MAX}, {@code COMPRESSION_MIN},
-     * {@code COMPRESSION_NONE} or {@code COMPRESSION_DEFAULT}
+     * <code>COMPRESSION_MAX</code>, <code>COMPRESSION_MIN</code>,
+     * <code>COMPRESSION_NONE</code> or <code>COMPRESSION_DEFAULT</code>
      * @param mode the mode in which the instance will operate. Can be either
-     * of {@code MODE_DEFLATER} or {@code MODE_INFLATER}
+     * of <code>MODE_DEFLATER</code> or <code>MODE_INFLATER</code>
      * @throws IllegalArgumentException if the mode is incorrect
      */
     public Zlib(int compressionLevel, int mode) {
-        switch (compressionLevel) {
-            case COMPRESSION_MAX:
-            case COMPRESSION_MIN:
-            case COMPRESSION_NONE:
-            case COMPRESSION_DEFAULT:
-                this.compressionLevel = compressionLevel;
-                break;
-            default:
-                throw new IllegalArgumentException("invalid compression level 
specified");
-        }
-
-        // create a new instance of ZStream. This will be done only once.
-        zStream = new ZStream();
-
-        switch (mode) {
-            case MODE_DEFLATER:
-                zStream.deflateInit(this.compressionLevel);
-                break;
-            case MODE_INFLATER:
-                zStream.inflateInit();
-                break;
-            default:
-                throw new IllegalArgumentException("invalid mode specified");
-        }
-
-        this.mode = mode;
+        this(compressionLevel, mode, MAX_DECOMPRESSED_SIZE, 
MAX_DECOMPRESS_RATIO, DECOMPRESS_RATIO_MIN_SIZE);
     }
-    
+
 
     /**
      * Creates an instance of the ZLib class.
-     * 
+     *
      * @param compressionLevel the level of compression that should be used. 
One of
      * <code>COMPRESSION_MAX</code>, <code>COMPRESSION_MIN</code>,
      * <code>COMPRESSION_NONE</code> or <code>COMPRESSION_DEFAULT</code>
      * @param mode the mode in which the instance will operate. Can be either
      * of <code>MODE_DEFLATER</code> or <code>MODE_INFLATER</code>
-     * @param maxDecompressedSize The maximum inflation size for a buffer. 
Default to 1MB
+     * @param maxDecompressedSize the maximum inflation size for a buffer
+     * @param maxDecompressRatio the maximum allowed ratio of decompressed to
+     * compressed bytes, evaluated cumulatively over the lifetime of this
+     * inflater. A value &lt;= 0 disables the check.
+     * @param decompressRatioMinSize the minimum cumulative decompressed size
+     * (in bytes) below which the ratio check is skipped.
      * @throws IllegalArgumentException if the mode is incorrect
      */
-    public Zlib(int compressionLevel, int mode, int maxDecompressedSize) {
+    public Zlib(int compressionLevel, int mode, int maxDecompressedSize,
+            long maxDecompressRatio, long decompressRatioMinSize) {
         switch (compressionLevel) {
             case COMPRESSION_MAX:
             case COMPRESSION_MIN:
@@ -139,6 +139,8 @@ class Zlib {
                 break;
             case MODE_INFLATER:
                 this.maxDecompressedSize = maxDecompressedSize;
+                this.maxDecompressRatio = maxDecompressRatio;
+                this.decompressRatioMinSize = decompressRatioMinSize;
                 zStream.inflateInit();
                 break;
             default:
@@ -147,7 +149,7 @@ class Zlib {
 
         this.mode = mode;
     }
-    
+
 
     /**
      * Uncompress the given buffer, returning it in a new buffer.
@@ -188,12 +190,14 @@ class Zlib {
                     case JZlib.Z_OK:
                         // completed decompression, lets copy data and get out
                     case JZlib.Z_BUF_ERROR:
-                        // Try to avoid exhausting the JVM memory by 
controling the resulting buffer 
+                        // Try to avoid exhausting the JVM memory by 
controling the resulting buffer
                         // size after inflation
                         if (outBuffer.position() + zStream.next_out_index > 
maxDecompressedSize) {
                             throw new IOException("decompressed size exceeds 
max " + maxDecompressedSize);
                         }
-                        
+
+                        checkDecompressRatio();
+
                         // need more space for output. store current output 
and get more
                         outBuffer.put(outBytes, 0, zStream.next_out_index);
                         zStream.next_out_index = 0;
@@ -255,13 +259,29 @@ class Zlib {
             }
 
             IoBuffer outBuf = IoBuffer.wrap(outBytes, 0, 
zStream.next_out_index);
-            
-            cleanUp();
 
+            cleanUp();
+            
             return outBuf;
         }
     }
 
+    /**
+     * Checks the cumulative decompression ratio against the configured 
maximum.
+     *
+     * @throws IOException if the cumulative ratio exceeds {@code 
maxDecompressRatio}
+     */
+    private void checkDecompressRatio() throws IOException {
+        if (maxDecompressRatio <= 0L) {
+            return;
+        }
+        long totalOut = zStream.getTotalOut();
+        long totalIn = zStream.getTotalIn();
+        if (totalIn > 0L && totalOut > decompressRatioMinSize && totalOut / 
totalIn > maxDecompressRatio) {
+            throw new IOException("decompression ratio " + (totalOut / 
totalIn) + " exceeds max " + maxDecompressRatio);
+        }
+    }
+
     /**
      * Cleans up the resources used by the compression library.
      */
diff --git 
a/mina-filter-compression/src/test/java/org/apache/mina/filter/compression/ZlibTest.java
 
b/mina-filter-compression/src/test/java/org/apache/mina/filter/compression/ZlibTest.java
index 3e73469c5..383a00aae 100644
--- 
a/mina-filter-compression/src/test/java/org/apache/mina/filter/compression/ZlibTest.java
+++ 
b/mina-filter-compression/src/test/java/org/apache/mina/filter/compression/ZlibTest.java
@@ -21,10 +21,12 @@ package org.apache.mina.filter.compression;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.Random;
 
 import org.apache.mina.core.buffer.IoBuffer;
 import org.junit.Before;
@@ -44,6 +46,26 @@ public class ZlibTest {
         inflater = new Zlib(Zlib.COMPRESSION_MAX, Zlib.MODE_INFLATER);
     }
 
+    private IoBuffer deflateZeros(int size) throws IOException {
+        try {
+            return new Zlib(Zlib.COMPRESSION_MAX, Zlib.MODE_DEFLATER)
+                    .deflate(IoBuffer.wrap(new byte[size]));
+        } catch (Exception e) {
+            throw new AssertionError("failed to deflate test fixture", e);
+        }
+    }
+
+    private IoBuffer deflateRandom(int size) throws IOException {
+        try {
+            byte[] data = new byte[size];
+            new Random(0).nextBytes(data);
+            return new Zlib(Zlib.COMPRESSION_MAX, Zlib.MODE_DEFLATER)
+                    .deflate(IoBuffer.wrap(data));
+        } catch (Exception e) {
+            throw new AssertionError("failed to deflate test fixture", e);
+        }
+    }
+
     @Test
     public void testCompression() throws Exception {
         String strInput = "";
@@ -137,14 +159,12 @@ public class ZlibTest {
      */
     @Test
     public void testZBombDataNoLimit() throws Exception {
-        // Create an inflater with no size limit
-        Zlib inflaterNoLimit = new Zlib(Zlib.COMPRESSION_MAX, 
Zlib.MODE_INFLATER);
+        // Create an inflater with no size limit and the ratio check disabled
+        Zlib inflaterNoLimit = new Zlib(Zlib.COMPRESSION_MAX, 
Zlib.MODE_INFLATER,
+                Zlib.MAX_DECOMPRESSED_SIZE, 0L, 0L);
 
         // Try a 10MB buffer bomb. Should succeed
-        byte[] uncompressed = new byte[1_024*1_024*10];
-        
-        IoBuffer byteInput = IoBuffer.wrap(uncompressed);
-        IoBuffer byteCompressed = deflater.deflate(byteInput);
+        IoBuffer byteCompressed = deflateZeros(1_024 * 1_024 * 10);
         
         // Should be fine
         inflaterNoLimit.inflate(byteCompressed);
@@ -161,26 +181,113 @@ public class ZlibTest {
      * </ul> 
      * @throws Exception
      */
-    @Test(expected=IOException.class)
+    @Test
     public void testZBombData() throws Exception {
-        // Create an inflater with a 1Mb size limit
-        Zlib inflaterWithLimit = new Zlib(Zlib.COMPRESSION_MAX, 
Zlib.MODE_INFLATER, 1_024*1_024);
+        // Create an inflater with a 1Mb size limit and the ratio check 
disabled
+        // so this test stays focused on the size limit.
+        Zlib inflaterWithLimit = new Zlib(Zlib.COMPRESSION_MAX, 
Zlib.MODE_INFLATER, 1_024*1_024, 0L, 0L);
+        // Both inputs are fed to the same inflater as a continuous zlib
+        // stream, so use the shared deflater rather than the fresh-stream
+        // deflateZeros() helper.
 
-        // Try a 1MB buffer bomb. Should succeed
-        byte[] uncompressed = new byte[1_024*1_024];
-        
-        IoBuffer byteInput = IoBuffer.wrap(uncompressed);
-        IoBuffer byteCompressed = deflater.deflate(byteInput);
-        
-        // Should be fine
-        inflaterWithLimit.inflate(byteCompressed);
-        
-        // Now try with a 1Mb +1 byte buffer
-        uncompressed = new byte[1_024*1_024+1];
-        byteInput = IoBuffer.wrap(uncompressed);
-        byteCompressed = deflater.deflate(byteInput);
-        
-        // Should now fail and throw a IoException
-        inflaterWithLimit.inflate(byteCompressed);  
+        // Right at the size limit: should succeed.
+        inflaterWithLimit.inflate(deflater.deflate(IoBuffer.wrap(new 
byte[1_024 * 1_024])));
+
+        // One byte over the size limit: should throw.
+        IoBuffer overLimit = deflater.deflate(IoBuffer.wrap(new byte[1_024 * 
1_024 + 1]));
+        assertThrows(IOException.class, () -> 
inflaterWithLimit.inflate(overLimit));
+    }
+
+
+    /**
+     * A highly compressible payload that exceeds both the default ratio (100)
+     * and the default ratio min-size threshold should be rejected by the
+     * inflater.
+     */
+    @Test
+    public void testDecompressRatioExceeded() throws Exception {
+        // 64KiB of zeros compresses to well under 64KiB/100 bytes.
+        int size = 64 * 1_024;
+        IoBuffer byteCompressed = deflateZeros(size);
+        int compressedSize = byteCompressed.remaining();
+        long actualCompressRatio = size / compressedSize;
+
+        // Inflater configured one ratio step below the actual payload's
+        // ratio: the inflate call must throw.
+        Zlib inflater = new Zlib(Zlib.COMPRESSION_MAX, Zlib.MODE_INFLATER, 
Zlib.MAX_DECOMPRESSED_SIZE, actualCompressRatio - 1, 0L);
+        assertThrows(IOException.class, () -> 
inflater.inflate(byteCompressed));
+    }
+
+
+    /**
+     * The ratio check must not fire while the cumulative decompressed size is
+     * below the configured min-size threshold.
+     */
+    @Test
+    public void testDecompressRatioBelowMinSize() throws Exception {
+        int size = 1_024 * 1_024;
+        IoBuffer byteCompressed = deflateZeros(size);
+
+        // Ratio of 100 would normally trip on this payload; raise the min-size
+        // threshold above the payload so the check is skipped.
+        Zlib inflater = new Zlib(Zlib.COMPRESSION_MAX, Zlib.MODE_INFLATER, 
Zlib.MAX_DECOMPRESSED_SIZE, 1L, size);
+        inflater.inflate(byteCompressed);
+    }
+
+
+    /**
+     * The ratio check is cumulative across multiple inflate() calls on the
+     * same stream, so a bomb cannot bypass it by being split into small
+     * fragments.
+     */
+    @Test
+    public void testDecompressRatioCumulative() throws Exception {
+        Zlib inflater = new Zlib(Zlib.COMPRESSION_MAX, Zlib.MODE_INFLATER, 
Zlib.MAX_DECOMPRESSED_SIZE, 1L, Zlib.DECOMPRESS_RATIO_MIN_SIZE);
+
+        // Below the min-size gate
+        int chunkSize = (int) Zlib.DECOMPRESS_RATIO_MIN_SIZE;
+        inflater.inflate(deflater.deflate(IoBuffer.wrap(new byte[chunkSize])));
+
+        // Exceeds the min-size gate
+        IoBuffer second = deflater.deflate(IoBuffer.wrap(new byte[chunkSize]));
+        assertThrows(IOException.class, () -> inflater.inflate(second));
+    }
+
+
+    /**
+     * An empty input buffer produces no decompressed output, so the ratio
+     * check must not fire even with a pathologically tight max ratio of 1
+     * and the min-size gate wide open.
+     */
+    @Test
+    public void testInflateEmptyBuffer() throws Exception {
+        Zlib inflater = new Zlib(Zlib.COMPRESSION_MAX, Zlib.MODE_INFLATER, 
Zlib.MAX_DECOMPRESSED_SIZE, 1L, 0L);
+        inflater.inflate(IoBuffer.allocate(0));
+    }
+
+
+    /**
+     * The default-constructor inflater must apply the documented defaults
+     * (max ratio = 100, min-size gate = 1 MiB). Three legs:
+     * <ul>
+     *   <li>Small + high ratio: zeros below the gate — must succeed (catches 
a min-size drop).</li>
+     *   <li>Large + low ratio: pseudo-random bytes above the gate — must 
succeed (catches an unintended max-ratio bump).</li>
+     *   <li>Large + high ratio: cumulative zeros above the gate — must throw 
(catches either default being effectively disabled).</li>
+     * </ul>
+     */
+    @Test
+    public void testDefaults() throws Exception {
+        // Leg 1: small + high ratio. Below the 1 MiB gate, check is skipped.
+        Zlib smallHighRatio = new Zlib(Zlib.COMPRESSION_MAX, 
Zlib.MODE_INFLATER);
+        smallHighRatio.inflate(deflateZeros(512 * 1_024));
+
+        // Leg 2: large + low ratio. Above the gate, but ratio ≈ 1 << 100.
+        Zlib largeLowRatio = new Zlib(Zlib.COMPRESSION_MAX, 
Zlib.MODE_INFLATER);
+        largeLowRatio.inflate(deflateRandom(2 * 1_024 * 1_024));
+
+        // Leg 3: large + high ratio. Above the gate, ratio >> 100, throws.
+        Zlib largeHighRatio = new Zlib(Zlib.COMPRESSION_MAX, 
Zlib.MODE_INFLATER);
+        IoBuffer bomb = deflateZeros(2 * 1_024 * 1_024);
+        assertThrows(IOException.class, () -> largeHighRatio.inflate(bomb));
     }
 }

Reply via email to