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 <= 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)); } } +
