CVE-2023-43642: Allocation of Resources Without Limits or Throttling (CWE-770)
value: snappy-java is a Java port of the snappy, a fast C++ compresser/decompresser developed by Google. The SnappyInputStream was found to be vulnerable to Denial of Service (DoS) attacks when decompressing data with a too large chunk size. Due to missing upper bound check on chunk length, an unrecoverable fatal error can occur. All versions of snappy-java including the latest released version 1.1.10.3 are vulnerable to this issue. A fix has been introduced in commit `9f8c3cf74` which will be included in the 1.1.10.4 release. Users are advised to upgrade. Users unable to upgrade should only accept compressed data from trusted sources.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 7.5
Published: 2023-09-25T20:15Z
Repository: https://github.com/xerial/snappy-java
Language: Java
Files changed: src/main/java/org/xerial/snappy/SnappyHadoopCompatibleOutputStream.java, src/main/java/org/xerial/snappy/SnappyInputStream.java, src/main/java/org/xerial/snappy/SnappyOutputStream.java, src/test/java/org/xerial/snappy/SnappyOutputStreamTest.java, src/test/java/org/xerial/snappy/SnappyTest.java
Commit date: 2023-09-23 15:55:18 -0700
Commit message: Merge pull request from GHSA-55g7-9cwv-5qfv

* Validate chunk size to be within a configured maximum

* Add constructors to have max size configurable

* Code cleanup

* Use 512MB for consistency

---------

Co-authored-by: Taro L. Saito <leo@xerial.org>
Diff: diff --git a/src/main/java/org/xerial/snappy/SnappyHadoopCompatibleOutputStream.java b/src/main/java/org/xerial/snappy/SnappyHadoopCompatibleOutputStream.java
index 67d2d188..d9f69bf4 100644
--- a/src/main/java/org/xerial/snappy/SnappyHadoopCompatibleOutputStream.java
+++ b/src/main/java/org/xerial/snappy/SnappyHadoopCompatibleOutputStream.java
@@ -1,11 +1,11 @@
 package org.xerial.snappy;
 
-import java.io.OutputStream;
-
 import org.xerial.snappy.buffer.CachedBufferAllocator;
 
+import java.io.OutputStream;
+
 public class SnappyHadoopCompatibleOutputStream extends SnappyOutputStream
 {
     public SnappyHadoopCompatibleOutputStream(OutputStream out)
     {
         this(out, DEFAULT_BLOCK_SIZE);
diff --git a/src/main/java/org/xerial/snappy/SnappyInputStream.java b/src/main/java/org/xerial/snappy/SnappyInputStream.java
index 9835cf90..7ccce1da 100755
--- a/src/main/java/org/xerial/snappy/SnappyInputStream.java
+++ b/src/main/java/org/xerial/snappy/SnappyInputStream.java
@@ -34,12 +34,15 @@ import java.io.InputStream;
  * @author leo
  */
 public class SnappyInputStream
         extends InputStream
 {
+    public static final int MAX_CHUNK_SIZE = 512 * 1024 * 1024; // 512 MiB
+
     private boolean finishedReading = false;
     protected final InputStream in;
+    private final int maxChunkSize;
 
     private byte[] compressed;
     private byte[] uncompressed;
     private int uncompressedCursor = 0;
     private int uncompressedLimit = 0;
@@ -53,10 +56,25 @@ public class SnappyInputStream
      * @throws IOException
      */
     public SnappyInputStream(InputStream input)
             throws IOException
     {
+        this(input, MAX_CHUNK_SIZE);
+    }
+
+
+    /**
+     * Create a filter for reading compressed data as a uncompressed stream with provided maximum chunk size
+     *
+     * @param input
+     * @param maxChunkSize
+     * @throws IOException
+     */
+    public SnappyInputStream(InputStream input, int maxChunkSize)
+            throws IOException
+    {
+        this.maxChunkSize = maxChunkSize;
         this.in = input;
         readHeader();
     }
 
     /**
@@ -420,10 +438,15 @@ public class SnappyInputStream
         // chunkSize is negative
         if (chunkSize < 0) {
             throw new SnappyError(SnappyErrorCode.INVALID_CHUNK_SIZE, "chunkSize is too big or negative : " + chunkSize);
         }
 
+        // chunkSize is big
+        if (chunkSize > maxChunkSize) {
+            throw new SnappyError(SnappyErrorCode.FAILED_TO_UNCOMPRESS, String.format("Received chunkSize %,d is greater than max configured chunk size %,d", chunkSize, maxChunkSize));
+        }
+
         // extend the compressed data buffer size
         if (compressed == null || chunkSize > compressed.length) {
             // chunkSize exceeds limit
             try {
                 compressed = new byte[chunkSize];
diff --git a/src/main/java/org/xerial/snappy/SnappyOutputStream.java b/src/main/java/org/xerial/snappy/SnappyOutputStream.java
index 0bab154f..290cc91b 100755
--- a/src/main/java/org/xerial/snappy/SnappyOutputStream.java
+++ b/src/main/java/org/xerial/snappy/SnappyOutputStream.java
@@ -57,10 +57,11 @@ import java.io.OutputStream;
  * @author leo
  */
 public class SnappyOutputStream
         extends OutputStream
 {
+    public static final int MAX_BLOCK_SIZE = 512 * 1024 * 1024; // 512 MiB
     static final int MIN_BLOCK_SIZE = 1 * 1024;
     static final int DEFAULT_BLOCK_SIZE = 32 * 1024; // Use 32kb for the default block size
 
     protected final OutputStream out;
     private final int blockSize;
@@ -82,21 +83,24 @@ public class SnappyOutputStream
     }
 
     /**
      * @param out
      * @param blockSize byte size of the internal buffer size
-     * @throws IOException
+     * @throws IllegalArgumentException when blockSize is larger than 512 MiB
      */
     public SnappyOutputStream(OutputStream out, int blockSize)
     {
         this(out, blockSize, CachedBufferAllocator.getBufferAllocatorFactory());
     }
 
     public SnappyOutputStream(OutputStream out, int blockSize, BufferAllocatorFactory bufferAllocatorFactory)
     {
         this.out = out;
         this.blockSize = Math.max(MIN_BLOCK_SIZE, blockSize);
+        if (this.blockSize > MAX_BLOCK_SIZE){
+            throw new IllegalArgumentException(String.format("Provided chunk size %,d larger than max %,d", this.blockSize, MAX_BLOCK_SIZE));
+        }
         int inputSize = blockSize;
         int outputSize = SnappyCodec.HEADER_SIZE + 4 + Snappy.maxCompressedLength(blockSize);
 
         this.inputBufferAllocator = bufferAllocatorFactory.getBufferAllocator(inputSize);
         this.outputBufferAllocator = bufferAllocatorFactory.getBufferAllocator(outputSize);
diff --git a/src/test/java/org/xerial/snappy/SnappyOutputStreamTest.java b/src/test/java/org/xerial/snappy/SnappyOutputStreamTest.java
index f2b7774a..d7831cbe 100755
--- a/src/test/java/org/xerial/snappy/SnappyOutputStreamTest.java
+++ b/src/test/java/org/xerial/snappy/SnappyOutputStreamTest.java
@@ -32,10 +32,11 @@ import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.nio.ByteOrder;
 
 import org.junit.Test;
+import org.junit.Assert;
 import org.xerial.snappy.buffer.BufferAllocatorFactory;
 import org.xerial.snappy.buffer.CachedBufferAllocator;
 import org.xerial.snappy.buffer.DefaultBufferAllocator;
 import org.xerial.util.FileResource;
 import org.xerial.util.log.Logger;
@@ -104,10 +105,21 @@ public class SnappyOutputStreamTest
         while (is.read(buf) != -1) {
         }
         is.close();
     }
 
+    @Test(expected = IllegalArgumentException.class)
+    public void invalidBlockSize()
+            throws Exception
+    {
+        // We rely on catch below, if there is no error this test will pass
+        // This can be done better with Assertions.assertThrows
+        Boolean exceptionThrown = false;
+        ByteArrayOutputStream b = new ByteArrayOutputStream();
+        SnappyOutputStream os = new SnappyOutputStream(b, 1024 * 1024 * 1024);
+    }
+
     @Test
     public void smallWrites()
             throws Exception
     {
 
diff --git a/src/test/java/org/xerial/snappy/SnappyTest.java b/src/test/java/org/xerial/snappy/SnappyTest.java
index 30edf66c..52c3a005 100755
--- a/src/test/java/org/xerial/snappy/SnappyTest.java
+++ b/src/test/java/org/xerial/snappy/SnappyTest.java
@@ -377,10 +377,28 @@ public class SnappyTest
             // Exception here will be catched
             // But OutOfMemoryError will not be caught, and will still be thrown
         }
     }
 
+    /*
+    Tests sad cases for SnappyInputStream.read method
+    - Expects a failed to compress exception due to upper bounds chunk size
+    - {-126, 'S', 'N', 'A', 'P', 'P', 'Y', 0, 0, 0, 0, 0, 0, 0, 0, 0,(byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff}
+     */
+    @Test
+    public void isInvalidChunkLengthForSnappyInputStream()
+            throws Exception {
+        byte[] data = {-126, 'S', 'N', 'A', 'P', 'P', 'Y', 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff};
+        SnappyInputStream in = new SnappyInputStream(new ByteArrayInputStream(data));
+        byte[] out = new byte[50];
+        try {
+            in.read(out);
+        } catch (SnappyError error) {
+            Assert.assertEquals(error.errorCode, SnappyErrorCode.FAILED_TO_UNCOMPRESS);
+        }
+    }
+
     /*
     Tests happy cases for BitShuffle.shuffle method
     - double: 0, 10
     - float: 0, 10
     - int: 0, 10

Vulnerable code: import java.io.OutputStream;

     * @throws IOException
Fixed code: import java.io.OutputStream;

    public static final int MAX_CHUNK_SIZE = 512 * 1024 * 1024; // 512 MiB

    private final int maxChunkSize;
        this(input, MAX_CHUNK_SIZE);
    }


    /**
     * Create a filter for reading compressed data as a uncompressed stream with provided maximum chunk size
     *
     * @param input
     * @param maxChunkSize
     * @throws IOException
     */
    public SnappyInputStream(InputStream input, int maxChunkSize)
            throws IOException
    {
        this.maxChunkSize = maxChunkSize;
        // chunkSize is big
        if (chunkSize > maxChunkSize) {
            throw new SnappyError(SnappyErrorCode.FAILED_TO_UNCOMPRESS, String.format("Received chunkSize %,d is greater than max configured chunk size %,d", chunkSize, maxChunkSize));
        }

    public static final int MAX_BLOCK_SIZE = 512 * 1024 * 1024; // 512 MiB
     * @throws IllegalArgumentException when blockSize is larger than 512 MiB
        if (this.blockSize > MAX_BLOCK_SIZE){
            throw new IllegalArgumentException(String.format("Provided chunk size %,d larger than max %,d", this.blockSize, MAX_BLOCK_SIZE));
        }
import org.junit.Assert;
    @Test(expected = IllegalArgumentException.class)
    public void invalidBlockSize()
            throws Exception
    {
        // We rely on catch below, if there is no error this test will pass
        // This can be done better with Assertions.assertThrows
        Boolean exceptionThrown = false;
        ByteArrayOutputStream b = new ByteArrayOutputStream();
        SnappyOutputStream os = new SnappyOutputStream(b, 1024 * 1024 * 1024);
    }

    /*
    Tests sad cases for SnappyInputStream.read method
    - Expects a failed to compress exception due to upper bounds chunk size
    - {-126, 'S', 'N', 'A', 'P', 'P', 'Y', 0, 0, 0, 0, 0, 0, 0, 0, 0,(byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff}
     */
    @Test
    public void isInvalidChunkLengthForSnappyInputStream()
            throws Exception {
        byte[] data = {-126, 'S', 'N', 'A', 'P', 'P', 'Y', 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff};
        SnappyInputStream in = new SnappyInputStream(new ByteArrayInputStream(data));
        byte[] out = new byte[50];
        try {
            in.read(out);
        } catch (SnappyError error) {
            Assert.assertEquals(error.errorCode, SnappyErrorCode.FAILED_TO_UNCOMPRESS);
        }
    }