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