CVE-2024-28102: Insufficient Information (NVD-CWE-noinfo)
value: JWCrypto implements JWK, JWS, and JWE specifications using python-cryptography. Prior to version 1.5.6, an attacker can cause a denial of service attack by passing in a malicious JWE Token with a high compression ratio. When the server processes this token, it will consume a lot of memory and processing time. Version 1.5.6 fixes this vulnerability by limiting the maximum token length.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-21T02:52Z
Repository: https://github.com/latchset/jwcrypto
Language: Python
Files changed: jwcrypto/jwe.py, jwcrypto/tests.py
Commit date: 2024-03-06 14:44:08 -0500
Commit message: Address potential DoS with high compression ratio

Fixes CVE-2024-28102

Signed-off-by: Simo Sorce <simo@redhat.com>

Diff: diff --git a/jwcrypto/jwe.py b/jwcrypto/jwe.py
index 9412881..5df500b 100644
--- a/jwcrypto/jwe.py
+++ b/jwcrypto/jwe.py
@@ -8,10 +8,13 @@ from jwcrypto.common import JWSEHeaderParameter, JWSEHeaderRegistry
 from jwcrypto.common import base64url_decode, base64url_encode
 from jwcrypto.common import json_decode, json_encode
 from jwcrypto.jwa import JWA
 from jwcrypto.jwk import JWKSet
 
+# Limit the amount of data we are willing to decompress by default.
+default_max_compressed_size = 256 * 1024
+
 
 # RFC 7516 - 4.1
 # name: (description, supported?)
 JWEHeaderRegistry = {
     'alg': JWSEHeaderParameter('Algorithm', False, True, None),
@@ -420,10 +423,14 @@ class JWE:
                                         self.objects['ciphertext'],
                                         self.objects['tag'])
 
         compress = jh.get('zip', None)
         if compress == 'DEF':
+            if len(data) > default_max_compressed_size:
+                raise InvalidJWEData(
+                    'Compressed data exceeds maximum allowed'
+                    'size' + f' ({default_max_compressed_size})')
             self.plaintext = zlib.decompress(data, -zlib.MAX_WBITS)
         elif compress is None:
             self.plaintext = data
         else:
             raise ValueError('Unknown compression')
diff --git a/jwcrypto/tests.py b/jwcrypto/tests.py
index bb2ff10..59049f8 100644
--- a/jwcrypto/tests.py
+++ b/jwcrypto/tests.py
@@ -2109,10 +2109,36 @@ class ConformanceTests(unittest.TestCase):
         with self.assertRaisesRegex(ValueError, 'too large'):
             p2cenc.add_recipient(key)
         jwa.default_max_pbkdf2_iterations += 2
         p2cenc.add_recipient(key)
 
+    def test_jwe_decompression_max(self):
+        key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * (128 // 8)))
+        payload = '{"u": "' + "u" * 400000000 + '", "uu":"' \
+            + "u" * 400000000 + '"}'
+        protected_header = {
+            "alg": "A128KW",
+            "enc": "A128GCM",
+            "typ": "JWE",
+            "zip": "DEF",
+        }
+        enc = jwe.JWE(payload.encode('utf-8'),
+                      recipient=key,
+                      protected=protected_header).serialize(compact=True)
+        with self.assertRaises(jwe.InvalidJWEData):
+            check = jwe.JWE()
+            check.deserialize(enc)
+            check.decrypt(key)
+
+        defmax = jwe.default_max_compressed_size
+        jwe.default_max_compressed_size = 1000000000
+        # ensure we can eraise the limit and decrypt
+        check = jwe.JWE()
+        check.deserialize(enc)
+        check.decrypt(key)
+        jwe.default_max_compressed_size = defmax
+
 
 class JWATests(unittest.TestCase):
     def test_jwa_create(self):
         for name, cls in jwa.JWA.algorithms_registry.items():
             self.assertEqual(cls.name, name)

Fixed code: # Limit the amount of data we are willing to decompress by default.
default_max_compressed_size = 256 * 1024

            if len(data) > default_max_compressed_size:
                raise InvalidJWEData(
                    'Compressed data exceeds maximum allowed'
                    'size' + f' ({default_max_compressed_size})')
    def test_jwe_decompression_max(self):
        key = jwk.JWK(kty='oct', k=base64url_encode(b'A' * (128 // 8)))
        payload = '{"u": "' + "u" * 400000000 + '", "uu":"' \
            + "u" * 400000000 + '"}'
        protected_header = {
            "alg": "A128KW",
            "enc": "A128GCM",
            "typ": "JWE",
            "zip": "DEF",
        }
        enc = jwe.JWE(payload.encode('utf-8'),
                      recipient=key,
                      protected=protected_header).serialize(compact=True)
        with self.assertRaises(jwe.InvalidJWEData):
            check = jwe.JWE()
            check.deserialize(enc)
            check.decrypt(key)

        defmax = jwe.default_max_compressed_size
        jwe.default_max_compressed_size = 1000000000
        # ensure we can eraise the limit and decrypt
        check = jwe.JWE()
        check.deserialize(enc)
        check.decrypt(key)
        jwe.default_max_compressed_size = defmax