CVE-2024-28180 (published 2024-03-09T01:15Z)

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.\n


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/go-jose/go-jose
commit_details: repo_url: https://github.com/go-jose/go-jose
author: Jacob Hoffman-Andrews
author_date: 2024-03-07 11:46:13-08:00
msg: jwe: limit maximum output from decompressing (#106)
repository: repo_url: https://github.com/go-jose/go-jose
repo_name: go-jose/go-jose
description: An implementation of JOSE standards (JWE, JWS, JWT) in Go
repo_language: Go
owner: go-jose
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,14 @@
+# 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

programming_language: Markdown
filename: crypter.go
change_type: ModificationType.MODIFY
diff: @@ -440,6 +440,9 @@ func (ctx *genericEncrypter) Options() EncrypterOptions {
 //
 // 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)
 
@@ -511,6 +514,9 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
 //
 // 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)
 

programming_language: Go
filename: encoding.go
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,7 @@ import (
 	"compress/flate"
 	"encoding/base64"
 	"encoding/binary"
+	"fmt"
 	"io"
 	"math/big"
 	"strings"
@@ -85,7 +86,7 @@ func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) {
 	}
 }
 
-// Compress with DEFLATE
+// deflate compresses the input.
 func deflate(input []byte) ([]byte, error) {
 	output := new(bytes.Buffer)
 
@@ -97,15 +98,24 @@ func deflate(input []byte) ([]byte, error) {
 	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

programming_language: Go
filename: encoding_test.go
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,8 @@ package jose
 
 import (
 	"bytes"
+	"crypto/rand"
+	"io"
 	"strings"
 	"testing"
 )
@@ -57,6 +59,38 @@ func TestInvalidCompression(t *testing.T) {
 	}
 }
 
+// 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}) {

programming_language: Go