CVE-2023-43642 (published 2023-09-25T20:15Z)

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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-770
cwe_name: Allocation of Resources Without Limits or Throttling
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.
extended_description: Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily.  Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.
         

Fix information:
repo_url: https://github.com/xerial/snappy-java
commit_details: repo_url: https://github.com/xerial/snappy-java
author: BD
author_date: 2023-09-24 04:25:18+05:30
msg: 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>
repository: repo_url: https://github.com/xerial/snappy-java
repo_name: xerial/snappy-java
description: Snappy compressor/decompressor for Java
repo_language: Java
owner: xerial
file_changes: filename: SnappyHadoopCompatibleOutputStream.java
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,9 @@
 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)

programming_language: Java
filename: SnappyInputStream.java
change_type: ModificationType.MODIFY
diff: @@ -36,8 +36,11 @@ import java.io.InputStream;
 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;
@@ -55,6 +58,21 @@ public class SnappyInputStream
     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();
     }
@@ -422,6 +440,11 @@ public class SnappyInputStream
             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

programming_language: Java
filename: SnappyOutputStream.java
change_type: ModificationType.MODIFY
diff: @@ -59,6 +59,7 @@ import java.io.OutputStream;
 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
 
@@ -84,7 +85,7 @@ 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)
     {
@@ -95,6 +96,9 @@ public class SnappyOutputStream
     {
         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);
 

programming_language: Java
filename: SnappyOutputStreamTest.java
change_type: ModificationType.MODIFY
diff: @@ -34,6 +34,7 @@ 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;
@@ -106,6 +107,17 @@ public class SnappyOutputStreamTest
         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

programming_language: Java
filename: SnappyTest.java
change_type: ModificationType.MODIFY
diff: @@ -379,6 +379,24 @@ public class SnappyTest
         }
     }
 
+    /*
+    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

programming_language: Java