From 50a6bfcd072155a4e13b11674534b02471dd7204 Mon Sep 17 00:00:00 2001 From: OwenSanzas Date: Wed, 14 Jan 2026 10:31:38 +0000 Subject: [PATCH 1/4] [AVRO-4081] Add decompression size limit to prevent decompression bomb DoS Add maximum decompression size limit in DeflateCodec to prevent OutOfMemoryError when processing maliciously crafted Avro files with high compression ratios (decompression bombs). The limit defaults to 200MB and can be configured via system property: org.apache.avro.limits.decompress.maxLength --- .../org/apache/avro/file/DeflateCodec.java | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java index e6d58e46a13..e50ace19287 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java +++ b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java @@ -20,11 +20,12 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; -import java.util.zip.InflaterOutputStream; +import org.apache.avro.AvroRuntimeException; import org.apache.avro.util.NonCopyingByteArrayOutputStream; /** @@ -38,6 +39,20 @@ public class DeflateCodec extends Codec { private static final int DEFAULT_BUFFER_SIZE = 8192; + private static final String MAX_DECOMPRESS_LENGTH_PROPERTY = "org.apache.avro.limits.decompress.maxLength"; + private static final long DEFAULT_MAX_DECOMPRESS_LENGTH = 200L * 1024 * 1024; // 200MB default limit + + private static long getMaxDecompressLength() { + String prop = System.getProperty(MAX_DECOMPRESS_LENGTH_PROPERTY); + if (prop != null) { + try { + return Long.parseLong(prop); + } catch (NumberFormatException e) { + // Use default + } + } + return DEFAULT_MAX_DECOMPRESS_LENGTH; + } static class Option extends CodecFactory { private final int compressionLevel; @@ -78,10 +93,32 @@ public ByteBuffer compress(ByteBuffer data) throws IOException { @Override public ByteBuffer decompress(ByteBuffer data) throws IOException { + long maxLength = getMaxDecompressLength(); NonCopyingByteArrayOutputStream baos = new NonCopyingByteArrayOutputStream(DEFAULT_BUFFER_SIZE); - try (OutputStream outputStream = new InflaterOutputStream(baos, getInflater())) { - outputStream.write(data.array(), computeOffset(data), data.remaining()); + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long totalBytes = 0; + + Inflater inflater = getInflater(); + inflater.setInput(data.array(), computeOffset(data), data.remaining()); + + try { + while (!inflater.finished()) { + int len = inflater.inflate(buffer); + if (len == 0 && inflater.needsInput()) { + break; + } + totalBytes += len; + if (totalBytes > maxLength) { + throw new AvroRuntimeException( + "Decompressed size " + totalBytes + " exceeds maximum allowed size " + maxLength + + ". This can be configured by setting the system property " + MAX_DECOMPRESS_LENGTH_PROPERTY); + } + baos.write(buffer, 0, len); + } + } catch (DataFormatException e) { + throw new IOException("Invalid deflate data", e); } + return baos.asByteBuffer(); } From 13edbf8f78135fa0fbfa578da8ec9d0ce1ea7c4a Mon Sep 17 00:00:00 2001 From: Ze Sheng <108382772+OwenSanzas@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:42:57 -0600 Subject: [PATCH 2/4] Update lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java Thanks! Co-authored-by: Martin Grigorov --- .../avro/src/main/java/org/apache/avro/file/DeflateCodec.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java index e50ace19287..d43b8dc9396 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java +++ b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java @@ -110,7 +110,7 @@ public ByteBuffer decompress(ByteBuffer data) throws IOException { totalBytes += len; if (totalBytes > maxLength) { throw new AvroRuntimeException( - "Decompressed size " + totalBytes + " exceeds maximum allowed size " + maxLength + "Decompressed size " + totalBytes + " (bytes) exceeds maximum allowed size " + maxLength + ". This can be configured by setting the system property " + MAX_DECOMPRESS_LENGTH_PROPERTY); } baos.write(buffer, 0, len); From 406bf50b7fb5e3aa459ad453b0c2aa03d4ef5427 Mon Sep 17 00:00:00 2001 From: Ze Sheng <108382772+OwenSanzas@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:43:09 -0600 Subject: [PATCH 3/4] Update lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java Co-authored-by: Martin Grigorov --- .../avro/src/main/java/org/apache/avro/file/DeflateCodec.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java index d43b8dc9396..319b8947c6a 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java +++ b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java @@ -111,7 +111,7 @@ public ByteBuffer decompress(ByteBuffer data) throws IOException { if (totalBytes > maxLength) { throw new AvroRuntimeException( "Decompressed size " + totalBytes + " (bytes) exceeds maximum allowed size " + maxLength - + ". This can be configured by setting the system property " + MAX_DECOMPRESS_LENGTH_PROPERTY); + + ". This can be configured by setting the system property '" + MAX_DECOMPRESS_LENGTH_PROPERTY + "'"); } baos.write(buffer, 0, len); } From fd85f2e62a94dfcce46f8554b33c2d1977de2606 Mon Sep 17 00:00:00 2001 From: OwenSanzas Date: Wed, 14 Jan 2026 20:03:23 +0000 Subject: [PATCH 4/4] Address code review feedback for decompression bomb fix - Move MAX_DECOMPRESS_LENGTH initialization to static block (read once at class load) - Add WARNING log for invalid property values (NumberFormatException) - Validate negative and zero values, reject with warning - Add "(bytes)" to error message for clarity - Add quotes around property name in error message Test command: java -Xmx64m -Dorg.apache.avro.limits.decompress.maxLength=1048576 \ -jar avro-tools-1.13.0-SNAPSHOT.jar tojson poc.avro Expected behavior: Exception in thread "main" org.apache.avro.AvroRuntimeException: Decompressed size 1056768 (bytes) exceeds maximum allowed size 1048576. This can be configured by setting the system property 'org.apache.avro.limits.decompress.maxLength' --- .../org/apache/avro/file/DeflateCodec.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java index 319b8947c6a..1568971d94a 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java +++ b/lang/java/avro/src/main/java/org/apache/avro/file/DeflateCodec.java @@ -27,6 +27,8 @@ import org.apache.avro.AvroRuntimeException; import org.apache.avro.util.NonCopyingByteArrayOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Implements DEFLATE (RFC1951) compression and decompression. @@ -38,20 +40,32 @@ */ public class DeflateCodec extends Codec { + private static final Logger LOG = LoggerFactory.getLogger(DeflateCodec.class); + private static final int DEFAULT_BUFFER_SIZE = 8192; private static final String MAX_DECOMPRESS_LENGTH_PROPERTY = "org.apache.avro.limits.decompress.maxLength"; private static final long DEFAULT_MAX_DECOMPRESS_LENGTH = 200L * 1024 * 1024; // 200MB default limit - private static long getMaxDecompressLength() { + private static final long MAX_DECOMPRESS_LENGTH; + + static { String prop = System.getProperty(MAX_DECOMPRESS_LENGTH_PROPERTY); + long limit = DEFAULT_MAX_DECOMPRESS_LENGTH; if (prop != null) { try { - return Long.parseLong(prop); + long parsed = Long.parseLong(prop); + if (parsed <= 0) { + LOG.warn("Invalid value '{}' for property '{}': must be positive. Using default: {}", prop, + MAX_DECOMPRESS_LENGTH_PROPERTY, DEFAULT_MAX_DECOMPRESS_LENGTH); + } else { + limit = parsed; + } } catch (NumberFormatException e) { - // Use default + LOG.warn("Could not parse property '{}' value '{}'. Using default: {}", MAX_DECOMPRESS_LENGTH_PROPERTY, prop, + DEFAULT_MAX_DECOMPRESS_LENGTH); } } - return DEFAULT_MAX_DECOMPRESS_LENGTH; + MAX_DECOMPRESS_LENGTH = limit; } static class Option extends CodecFactory { @@ -93,7 +107,6 @@ public ByteBuffer compress(ByteBuffer data) throws IOException { @Override public ByteBuffer decompress(ByteBuffer data) throws IOException { - long maxLength = getMaxDecompressLength(); NonCopyingByteArrayOutputStream baos = new NonCopyingByteArrayOutputStream(DEFAULT_BUFFER_SIZE); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; long totalBytes = 0; @@ -108,9 +121,9 @@ public ByteBuffer decompress(ByteBuffer data) throws IOException { break; } totalBytes += len; - if (totalBytes > maxLength) { + if (totalBytes > MAX_DECOMPRESS_LENGTH) { throw new AvroRuntimeException( - "Decompressed size " + totalBytes + " (bytes) exceeds maximum allowed size " + maxLength + "Decompressed size " + totalBytes + " (bytes) exceeds maximum allowed size " + MAX_DECOMPRESS_LENGTH + ". This can be configured by setting the system property '" + MAX_DECOMPRESS_LENGTH_PROPERTY + "'"); } baos.write(buffer, 0, len);