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 55b21337d..f6e174771 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
@@ -104,16 +104,22 @@ public class CompressionFilter extends IoFilterAdapter {
private boolean compressOutbound = true;
private int compressionLevel;
-
+
/** 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 CompressionFilter() {
* {@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 CompressionFilter(final int compressionLevel) {
*/
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,15 +165,43 @@ public CompressionFilter(final boolean compressInbound, final boolean compressOu
* {@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,
+ 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 true if data read is to be decompressed
+ * @param compressOutbound true 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;
}
-
+
+
/**
* {@inheritDoc}
*/
@@ -239,8 +273,8 @@ public void onPreAdd(IoFilterChain parent, String name, NextFilter nextFilter) t
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 44f315b76..c78abe63b 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,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.
+ *
+ *
Below this threshold the check is skipped to avoid false positives on small payloads where framing/header + * overhead dominates the ratio.
+ */ + /* 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,47 +97,28 @@ 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); } - + /** * Creates an instance of the ZLib class. - * + * * @param compressionLevel the level of compression that should be used. One of *COMPRESSION_MAX, COMPRESSION_MIN,
* COMPRESSION_NONE or COMPRESSION_DEFAULT
* @param mode the mode in which the instance will operate. Can be either
* of MODE_DEFLATER or MODE_INFLATER
- * @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 @@ public Zlib(int compressionLevel, int mode, int maxDecompressedSize) {
break;
case MODE_INFLATER:
this.maxDecompressedSize = maxDecompressedSize;
+ this.maxDecompressRatio = maxDecompressRatio;
+ this.decompressRatioMinSize = decompressRatioMinSize;
zStream.inflateInit();
break;
default:
@@ -147,7 +149,7 @@ public Zlib(int compressionLevel, int mode, int maxDecompressedSize) {
this.mode = mode;
}
-
+
/**
* Uncompress the given buffer, returning it in a new buffer.
@@ -188,12 +190,14 @@ public IoBuffer inflate(IoBuffer inBuffer) throws IOException {
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;
@@ -262,6 +266,22 @@ public IoBuffer deflate(IoBuffer inBuffer) throws IOException {
}
}
+ /**
+ * 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..c28a24abc 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 @@
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 void setUp() throws Exception {
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 void testFragments() throws Exception {
*