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 0b8ff7f509d13477574371a8e452a47f1b7ca67e Author: Emmanuel Lécharny <[email protected]> AuthorDate: Tue May 19 09:39:13 2026 +0200 Added a size limit for the inflated buffer --- .../mina/filter/compression/CompressionFilter.java | 45 ++++++-- .../org/apache/mina/filter/compression/Zlib.java | 120 +++++++++++++++------ .../apache/mina/filter/compression/ZlibTest.java | 60 +++++++++++ 3 files changed, 187 insertions(+), 38 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 b74271a7f..9e81cc3d7 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 @@ -52,6 +52,11 @@ import org.apache.mina.core.write.WriteRequest; * <p> * It goes without saying that the other end of this stream should also have a * compatible compressor/decompressor using the same algorithm. + * <p> + * Note: a inflater limit has been added to protect the application from ZBomb + * (a compressed buffer that when inflated will create a giant buffer). + * It can be set using the CompressionFilter constructor, passing a forth argument + * with the expected limit. * * @author <a href="http://mina.apache.org">Apache MINA Project</a> */ @@ -91,20 +96,24 @@ public class CompressionFilter extends IoFilterAdapter { /** * A flag that allows you to disable compression once. */ - public static final AttributeKey DISABLE_COMPRESSION_ONCE = new AttributeKey(CompressionFilter.class, "disableOnce"); + public static final AttributeKey DISABLE_COMPRESSION_ONCE = + new AttributeKey(CompressionFilter.class, "disableOnce"); private boolean compressInbound = true; private boolean compressOutbound = true; private int compressionLevel; + + /** The maximum decompressed size, to avoid an OOM. Default to 1Mb */ + private int maxDecompressedSize; /** * Creates a new instance which compresses outboud data and decompresses * inbound data with default compression level. */ public CompressionFilter() { - this(true, true, COMPRESSION_DEFAULT); + this(true, true, COMPRESSION_DEFAULT, Zlib.MAX_DECOMPRESSED_SIZE); } /** @@ -118,7 +127,7 @@ public class CompressionFilter extends IoFilterAdapter { * {@link #COMPRESSION_NONE}. */ public CompressionFilter(final int compressionLevel) { - this(true, true, compressionLevel); + this(true, true, compressionLevel, Zlib.MAX_DECOMPRESSED_SIZE); } /** @@ -132,10 +141,31 @@ public class CompressionFilter extends IoFilterAdapter { * {@link #COMPRESSION_MIN}, and * {@link #COMPRESSION_NONE}. */ - public CompressionFilter(final boolean compressInbound, final boolean compressOutbound, final int compressionLevel) { + public CompressionFilter(final boolean compressInbound, final boolean compressOutbound, + final int compressionLevel) { + this(true, true, compressionLevel, Zlib.MAX_DECOMPRESSED_SIZE); + } + + /** + * Creates a new instance. + * <p> + * Use thgis constructor if you want to set a limit to the inflated buffer size. + * + * @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 some data + */ + public CompressionFilter(final boolean compressInbound, final boolean compressOutbound, + final int compressionLevel, final int maxDecompressedSize) { this.compressionLevel = compressionLevel; this.compressInbound = compressInbound; this.compressOutbound = compressOutbound; + this.maxDecompressedSize = maxDecompressedSize; } /** @@ -170,7 +200,10 @@ public class CompressionFilter extends IoFilterAdapter { } /* - * @see org.apache.mina.core.IoFilter#filterWrite(org.apache.mina.core.IoFilter.NextFilter, org.apache.mina.core.IoSession, org.apache.mina.core.IoFilter.WriteRequest) + * @see org.apache.mina.core.IoFilter#filterWrite( + * org.apache.mina.core.IoFilter.NextFilter, + * org.apache.mina.core.IoSession, + * org.apache.mina.core.IoFilter.WriteRequest) */ protected Object doFilterWrite(NextFilter nextFilter, IoSession session, WriteRequest writeRequest) throws IOException { @@ -207,7 +240,7 @@ public class CompressionFilter extends IoFilterAdapter { } Zlib deflater = new Zlib(compressionLevel, Zlib.MODE_DEFLATER); - Zlib inflater = new Zlib(compressionLevel, Zlib.MODE_INFLATER); + Zlib inflater = new Zlib(compressionLevel, Zlib.MODE_INFLATER, maxDecompressedSize); 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 e8570089f..e76395787 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 @@ -54,6 +54,12 @@ class Zlib { /** The requested compression level */ private int compressionLevel; + + /** The maximum size of an inflated buffer. Default to 1Mb */ + /* Package protected */ + static final int MAX_DECOMPRESSED_SIZE = Integer.MAX_VALUE; + + private int maxDecompressedSize = MAX_DECOMPRESSED_SIZE; /** The inner stream used to inflate or deflate the data */ private ZStream zStream = null; @@ -73,31 +79,75 @@ class Zlib { */ 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"); + 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"); + case MODE_DEFLATER: + zStream.deflateInit(this.compressionLevel); + break; + case MODE_INFLATER: + zStream.inflateInit(); + break; + default: + throw new IllegalArgumentException("invalid mode specified"); } + + this.mode = mode; + } + + + /** + * 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 + * @throws IllegalArgumentException if the mode is incorrect + */ + public Zlib(int compressionLevel, int mode, int maxDecompressedSize) { + 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: + this.maxDecompressedSize = maxDecompressedSize; + zStream.inflateInit(); + break; + default: + throw new IllegalArgumentException("invalid mode specified"); + } + this.mode = mode; } + /** * Uncompress the given buffer, returning it in a new buffer. @@ -135,22 +185,28 @@ class Zlib { do { retval = zStream.inflate(JZlib.Z_SYNC_FLUSH); switch (retval) { - case JZlib.Z_OK: - // completed decompression, lets copy data and get out - case JZlib.Z_BUF_ERROR: - // need more space for output. store current output and get more - outBuffer.put(outBytes, 0, zStream.next_out_index); - zStream.next_out_index = 0; - zStream.avail_out = outBytes.length; - break; - default: - // unknown error - outBuffer = null; - if (zStream.msg == null) { - throw new IOException("Unknown error. Error code : " + retval); - } else { - throw new IOException("Unknown error. Error code : " + retval + " and message : " + zStream.msg); - } + 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 + // size after inflation + if (outBuffer.position() + zStream.next_out_index > maxDecompressedSize) { + throw new IOException("decompressed size exceeds max " + maxDecompressedSize); + } + + // need more space for output. store current output and get more + outBuffer.put(outBytes, 0, zStream.next_out_index); + zStream.next_out_index = 0; + zStream.avail_out = outBytes.length; + break; + default: + // unknown error + outBuffer = null; + if (zStream.msg == null) { + throw new IOException("Unknown error. Error code : " + retval); + } else { + throw new IOException("Unknown error. Error code : " + retval + " and message : " + zStream.msg); + } } } while (zStream.avail_in > 0); 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 a3c3da493..af053e1b0 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 @@ -126,4 +126,64 @@ public class ZlibTest { String strOutput = byteUncompressed.getString(Charset.forName("UTF8").newDecoder()); assertTrue(strOutput.equals(strInput)); } + + + /** + * Test the inflater with no limit + * We create buffers of various sizes: + * <ul> + * <li>A 1MB buffer that once compressed should inflate properly + * <li>A 10MB buffer that once compressed should inflate properly + * <li> + * </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); + + // 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); + + // Should be fine + inflaterNoLimit.inflate(byteCompressed); + } + + + /** + * Test the inflater default limit. + * We create buffers of various sizes: + * <ul> + * <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> + * @throws Exception + */ + @Test(expected=IOException.class) + 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); + + // 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); + } }
