CVE-2024-28180: Insufficient Information (NVD-CWE-noinfo)
value: Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. An attacker could send a JWE containing compressed data that used large amounts of memory and CPU when decompressed by Decrypt or DecryptMulti. Those functions now return an error if the decompressed data would exceed 250kB or 10x the compressed size (whichever is larger). This vulnerability has been patched in versions 4.0.1, 3.0.3 and 2.6.3.

CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-09T01:15Z
Repository: https://github.com/go-jose/go-jose
Language: Go
Files changed: CHANGELOG.md, crypter.go, encoding.go, encoding_test.go
Commit date: 2024-03-07 11:46:13 -0800
Commit message: jwe: limit maximum output from decompressing (#106)


Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08e43ab..28bdd2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
+# v4.0.1
+
+## Fixed
+
+ - An attacker could send a JWE containing compressed data that used large
+   amounts of memory and CPU when decompressed by `Decrypt` or `DecryptMulti`.
+   Those functions now return an error if the decompressed data would exceed
+   250kB or 10x the compressed size (whichever is larger). Thanks to
+   Enze Wang@Alioth and Jianjun Chen@Zhongguancun Lab (@zer0yu and @chenjj)
+   for reporting.
+
 # v4.0.0
 
 This release makes some breaking changes in order to more thoroughly
 address the vulnerabilities discussed in [Three New Attacks Against JSON Web
 Tokens][1], "Sign/encrypt confusion", "Billion hash attack", and "Polyglot
diff --git a/crypter.go b/crypter.go
index 073767f..aba0842 100644
--- a/crypter.go
+++ b/crypter.go
@@ -438,10 +438,13 @@ func (ctx *genericEncrypter) Options() EncrypterOptions {
 //   - string (a symmetric key)
 //   - Any type that satisfies the OpaqueKeyDecrypter interface.
 //
 // Note that ed25519 is only available for signatures, not encryption, so is
 // not an option here.
+//
+// Automatically decompresses plaintext, but returns an error if the decompressed
+// data would be >250kB or >10x the size of the compressed data, whichever is larger.
 func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) {
 	headers := obj.mergedHeaders(nil)
 
 	if len(obj.recipients) > 1 {
 		return nil, errors.New("go-jose/go-jose: too many recipients in payload; expecting only one")
@@ -509,10 +512,13 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
 // for which the decryption was successful, the merged headers for that recipient,
 // and the plaintext.
 //
 // The decryptionKey argument must have one of the types allowed for the
 // decryptionKey argument of Decrypt().
+//
+// Automatically decompresses plaintext, but returns an error if the decompressed
+// data would be >250kB or >3x the size of the compressed data, whichever is larger.
 func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) {
 	globalHeaders := obj.mergedHeaders(nil)
 
 	critical, err := globalHeaders.getCritical()
 	if err != nil {
diff --git a/encoding.go b/encoding.go
index bc71404..4f6e0d4 100644
--- a/encoding.go
+++ b/encoding.go
@@ -19,10 +19,11 @@ package jose
 import (
 	"bytes"
 	"compress/flate"
 	"encoding/base64"
 	"encoding/binary"
+	"fmt"
 	"io"
 	"math/big"
 	"strings"
 	"unicode"
 
@@ -83,11 +84,11 @@ func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) {
 	default:
 		return nil, ErrUnsupportedAlgorithm
 	}
 }
 
-// Compress with DEFLATE
+// deflate compresses the input.
 func deflate(input []byte) ([]byte, error) {
 	output := new(bytes.Buffer)
 
 	// Writing to byte buffer, err is always nil
 	writer, _ := flate.NewWriter(output, 1)
@@ -95,19 +96,28 @@ func deflate(input []byte) ([]byte, error) {
 
 	err := writer.Close()
 	return output.Bytes(), err
 }
 
-// Decompress with DEFLATE
+// inflate decompresses the input.
+//
+// Errors if the decompressed data would be >250kB or >10x the size of the
+// compressed data, whichever is larger.
 func inflate(input []byte) ([]byte, error) {
 	output := new(bytes.Buffer)
 	reader := flate.NewReader(bytes.NewBuffer(input))
 
-	_, err := io.Copy(output, reader)
-	if err != nil {
+	maxCompressedSize := max(250_000, 10*int64(len(input)))
+
+	limit := maxCompressedSize + 1
+	n, err := io.CopyN(output, reader, limit)
+	if err != nil && err != io.EOF {
 		return nil, err
 	}
+	if n == limit {
+		return nil, fmt.Errorf("uncompressed data would be too large (>%d bytes)", maxCompressedSize)
+	}
 
 	err = reader.Close()
 	return output.Bytes(), err
 }
 
diff --git a/encoding_test.go b/encoding_test.go
index fc48685..4b1d451 100644
--- a/encoding_test.go
+++ b/encoding_test.go
@@ -16,10 +16,12 @@
 
 package jose
 
 import (
 	"bytes"
+	"crypto/rand"
+	"io"
 	"strings"
 	"testing"
 )
 
 func TestDeflateRoundtrip(t *testing.T) {
@@ -55,10 +57,42 @@ func TestInvalidCompression(t *testing.T) {
 	if err == nil {
 		t.Error("should not accept invalid data")
 	}
 }
 
+// TestLargeZip tests that we can decompress a large input, so long as its
+// compression ratio is reasonable.
+func TestLargeZip(t *testing.T) {
+	input := new(bytes.Buffer)
+	_, err := io.CopyN(input, rand.Reader, 251_000)
+	if err != nil {
+		t.Fatalf("generating input: %s", err)
+	}
+	compressed, err := compress(DEFLATE, input.Bytes())
+	if err != nil {
+		t.Errorf("compressing: %s", err)
+	}
+	t.Logf("compression ratio: %g", float64(len(input.Bytes()))/float64(len(compressed)))
+	_, err = decompress(DEFLATE, compressed)
+	if err != nil {
+		t.Errorf("decompressing large input with low compression ratio: %s", err)
+	}
+}
+
+func TestZipBomb(t *testing.T) {
+	input := strings.Repeat("a", 251_000)
+	compressed, err := compress(DEFLATE, []byte(input))
+	if err != nil {
+		t.Errorf("compressing: %s", err)
+	}
+	t.Logf("compression ratio: %d %g", len(compressed), float64(len(input))/float64(len(compressed)))
+	out, err := decompress(DEFLATE, compressed)
+	if err == nil {
+		t.Errorf("expected error decompressing zip bomb, got none. output size %d", len(out))
+	}
+}
+
 func TestByteBufferTrim(t *testing.T) {
 	buf := newBufferFromInt(1)
 	if !bytes.Equal(buf.data, []byte{1}) {
 		t.Error("Byte buffer for integer '1' should contain [0x01]")
 	}

Vulnerable code: // Compress with DEFLATE
// Decompress with DEFLATE
	_, err := io.Copy(output, reader)
	if err != nil {
Fixed code: # v4.0.1

## Fixed

 - An attacker could send a JWE containing compressed data that used large
   amounts of memory and CPU when decompressed by `Decrypt` or `DecryptMulti`.
   Those functions now return an error if the decompressed data would exceed
   250kB or 10x the compressed size (whichever is larger). Thanks to
   Enze Wang@Alioth and Jianjun Chen@Zhongguancun Lab (@zer0yu and @chenjj)
   for reporting.

//
// Automatically decompresses plaintext, but returns an error if the decompressed
// data would be >250kB or >10x the size of the compressed data, whichever is larger.
//
// Automatically decompresses plaintext, but returns an error if the decompressed
// data would be >250kB or >3x the size of the compressed data, whichever is larger.
	"fmt"
// deflate compresses the input.
// inflate decompresses the input.
//
// Errors if the decompressed data would be >250kB or >10x the size of the
// compressed data, whichever is larger.
	maxCompressedSize := max(250_000, 10*int64(len(input)))

	limit := maxCompressedSize + 1
	n, err := io.CopyN(output, reader, limit)
	if err != nil && err != io.EOF {
	if n == limit {
		return nil, fmt.Errorf("uncompressed data would be too large (>%d bytes)", maxCompressedSize)
	}
	"crypto/rand"
	"io"
// TestLargeZip tests that we can decompress a large input, so long as its
// compression ratio is reasonable.
func TestLargeZip(t *testing.T) {
	input := new(bytes.Buffer)
	_, err := io.CopyN(input, rand.Reader, 251_000)
	if err != nil {
		t.Fatalf("generating input: %s", err)
	}
	compressed, err := compress(DEFLATE, input.Bytes())
	if err != nil {
		t.Errorf("compressing: %s", err)
	}
	t.Logf("compression ratio: %g", float64(len(input.Bytes()))/float64(len(compressed)))
	_, err = decompress(DEFLATE, compressed)
	if err != nil {
		t.Errorf("decompressing large input with low compression ratio: %s", err)
	}
}

func TestZipBomb(t *testing.T) {
	input := strings.Repeat("a", 251_000)
	compressed, err := compress(DEFLATE, []byte(input))
	if err != nil {
		t.Errorf("compressing: %s", err)
	}
	t.Logf("compression ratio: %d %g", len(compressed), float64(len(input))/float64(len(compressed)))
	out, err := decompress(DEFLATE, compressed)
	if err == nil {
		t.Errorf("expected error decompressing zip bomb, got none. output size %d", len(out))
	}
}