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

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

commit 1c4fb9002d7144907ca56b8ebfc440768928dc97
Author: Emmanuel Lécharny <[email protected]>
AuthorDate: Sun May 24 08:38:15 2026 +0200

    Backporting changes made in 2.2.X
---
 .../mina/filter/compression/CompressionFilter.java |   2 +-
 .../org/apache/mina/filter/compression/Zlib.java   |  78 ++++++----
 .../apache/mina/filter/compression/ZlibTest.java   | 167 +++++++++++++++++----
 3 files changed, 188 insertions(+), 59 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 a3a7c73d5..2c75174ae 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
@@ -327,7 +327,7 @@ public class CompressionFilter extends IoFilterAdapter {
     }
     
     /**
-     * Set the max alloweed compression ratio. If the inflated buffer exceed 
this ratio,
+     * Set the max allowed compression ratio. If the inflated buffer exceed 
this ratio,
      * an error will be generated. Note that the  
<code>decompressRatioMinSize</code> parameter
      * can be used to avoid bailing out for small inflated files with a high 
compression ratio.
      * 
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 e76395787..564df4397 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
@@ -59,8 +59,27 @@ class Zlib {
     /* 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;
 
@@ -78,32 +97,7 @@ class Zlib {
      * @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);
     }
     
 
@@ -115,10 +109,16 @@ class Zlib {
      * <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:
@@ -193,7 +195,9 @@ class Zlib {
                         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;
@@ -262,6 +266,22 @@ class Zlib {
         }
     }
 
+    /**
+     * 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 af053e1b0..e812c62cf 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 = "";
@@ -135,25 +157,23 @@ public class ZlibTest {
      *   <li>A 1MB buffer that once compressed should inflate properly
      *   <li>A 10MB buffer that once compressed should inflate properly
      *   <li>
-     * </ul> 
+     * </ul>
      * @throws Exception
      */
     @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);
     }
 
-    
+
     /**
      * Test the inflater default limit.
      * We create buffers of various sizes:
@@ -161,29 +181,118 @@ public class ZlibTest {
      *   <li>A 1MB Buffer that once compressed should inflate properly
      *   <li>A 1MB+1byte buffer that once compressed should generate an 
exception when inflated
      *   <li>
-     * </ul> 
+     * </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);
 
-        // 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);  
+        // 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.
+
+        // 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