From b91c69c4d2e63492102891a64ec9e1ea1009ccc7 Mon Sep 17 00:00:00 2001 From: Alistair Watts Date: Wed, 5 Feb 2025 12:43:50 +0000 Subject: [PATCH 1/9] Fix for CVE-2024-33664. JWE limited to 250K --- jose/constants.py | 2 ++ jose/jwe.py | 24 ++++++++++++++++++------ tests/test_jwe.py | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/jose/constants.py b/jose/constants.py index ab4d74d3..58787d46 100644 --- a/jose/constants.py +++ b/jose/constants.py @@ -96,3 +96,5 @@ class Zips: ZIPS = Zips() + +JWE_SIZE_LIMIT = 250 * 1024 diff --git a/jose/jwe.py b/jose/jwe.py index 2c387ff4..45e0d6e4 100644 --- a/jose/jwe.py +++ b/jose/jwe.py @@ -6,7 +6,7 @@ from . import jwk from .backends import get_random_bytes -from .constants import ALGORITHMS, ZIPS +from .constants import ALGORITHMS, ZIPS, JWE_SIZE_LIMIT from .exceptions import JWEError, JWEParseError from .utils import base64url_decode, base64url_encode, ensure_binary @@ -76,6 +76,13 @@ def decrypt(jwe_str, key): >>> jwe.decrypt(jwe_string, 'asecret128bitkey') 'Hello, World!' """ + + # Limit the token size - if the data is compressed then decompressing the + # data could lead to large memory usage. This helps address This addresses + # CVE-2024-33664. Also see _decompress() + if len(jwe_str) > JWE_SIZE_LIMIT: + raise JWEError(f"JWE string exceeds {JWE_SIZE_LIMIT} bytes") + header, encoded_header, encrypted_key, iv, cipher_text, auth_tag = _jwe_compact_deserialize(jwe_str) # Verify that the implementation understands and can process all @@ -424,13 +431,13 @@ def _compress(zip, plaintext): (bytes): Compressed plaintext """ if zip not in ZIPS.SUPPORTED: - raise NotImplementedError("ZIP {} is not supported!") + raise NotImplementedError(f"ZIP {zip} is not supported!") if zip is None: compressed = plaintext elif zip == ZIPS.DEF: compressed = zlib.compress(plaintext) else: - raise NotImplementedError("ZIP {} is not implemented!") + raise NotImplementedError(f"ZIP {zip} is not implemented!") return compressed @@ -446,13 +453,18 @@ def _decompress(zip, compressed): (bytes): Compressed plaintext """ if zip not in ZIPS.SUPPORTED: - raise NotImplementedError("ZIP {} is not supported!") + raise NotImplementedError(f"ZIP {zip} is not supported!") if zip is None: decompressed = compressed elif zip == ZIPS.DEF: - decompressed = zlib.decompress(compressed) + # If, during decompression, there is more data than expected, the + # decompression halts and raise an error. This addresses CVE-2024-33664 + decompressor = zlib.decompressobj() + decompressed = decompressor.decompress(compressed, max_length=JWE_SIZE_LIMIT) + if decompressor.unconsumed_tail: + raise JWEError(f"Decompressed JWE string exceeds {JWE_SIZE_LIMIT} bytes") else: - raise NotImplementedError("ZIP {} is not implemented!") + raise NotImplementedError(f"ZIP {zip} is not implemented!") return decompressed diff --git a/tests/test_jwe.py b/tests/test_jwe.py index f089d565..8c5ff387 100644 --- a/tests/test_jwe.py +++ b/tests/test_jwe.py @@ -5,7 +5,7 @@ import jose.backends from jose import jwe from jose.constants import ALGORITHMS, ZIPS -from jose.exceptions import JWEParseError +from jose.exceptions import JWEParseError, JWEError from jose.jwk import AESKey, RSAKey from jose.utils import base64url_decode @@ -525,3 +525,35 @@ def test_kid_header_not_present_when_not_provided(self): encrypted = jwe.encrypt("Text", PUBLIC_KEY_PEM, enc, alg) header = json.loads(base64url_decode(encrypted.split(b".")[0])) assert "kid" not in header + + @pytest.mark.skipif(AESKey is None, reason="No AES backend") + def test_jwe_with_excessive_data(self): + enc = ALGORITHMS.A256CBC_HS512 + alg = ALGORITHMS.RSA_OAEP_256 + import jose.constants + old_limit = jose.constants.JWE_SIZE_LIMIT + try: + jose.constants.JWE_SIZE_LIMIT = 1024 + encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg) + header = json.loads(base64url_decode(encrypted.split(b".")[0])) + with pytest.raises(JWEError): + actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) + finally: + jose.constants.JWE_SIZE_LIMIT = old_limit + + @pytest.mark.skipif(AESKey is None, reason="No AES backend") + def test_jwe_zip_with_excessive_data(self): + # Test that a fix for CVE-2024-33664 is in place. + enc = ALGORITHMS.A256CBC_HS512 + alg = ALGORITHMS.RSA_OAEP_256 + import jose.constants + old_limit = jose.constants.JWE_SIZE_LIMIT + try: + jose.constants.JWE_SIZE_LIMIT = 1024 + encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg, zip=ZIPS.DEF) + assert len(encrypted) < jose.constants.JWE_SIZE_LIMIT + header = json.loads(base64url_decode(encrypted.split(b".")[0])) + with pytest.raises(JWEError): + actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) + finally: + jose.constants.JWE_SIZE_LIMIT = old_limit From 17db60b04f6cb954706f84b30918a50a1d34b469 Mon Sep 17 00:00:00 2001 From: Alistair Watts Date: Thu, 6 Feb 2025 09:34:32 +0000 Subject: [PATCH 2/9] Removed Py3.7 from Ubuntu latest. Linting on Py3.10 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 049a7534..aee3dd9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: - os: macos-latest @@ -56,7 +56,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: 3.10 - name: Install dependencies run: | pip install -U setuptools From 5b23bd3ca87a2669e0c834878649bbc088fe6e1f Mon Sep 17 00:00:00 2001 From: Alistair Watts Date: Thu, 6 Feb 2025 10:11:37 +0000 Subject: [PATCH 3/9] Changed exception message --- jose/jwe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jose/jwe.py b/jose/jwe.py index 45e0d6e4..91343f3b 100644 --- a/jose/jwe.py +++ b/jose/jwe.py @@ -81,7 +81,7 @@ def decrypt(jwe_str, key): # data could lead to large memory usage. This helps address This addresses # CVE-2024-33664. Also see _decompress() if len(jwe_str) > JWE_SIZE_LIMIT: - raise JWEError(f"JWE string exceeds {JWE_SIZE_LIMIT} bytes") + raise JWEError(f"JWE string {len(jwe_str)} bytes exceeds {JWE_SIZE_LIMIT} bytes") header, encoded_header, encrypted_key, iv, cipher_text, auth_tag = _jwe_compact_deserialize(jwe_str) From af6272ceb90d5fa3df6d2b73d64a41ef1ff65006 Mon Sep 17 00:00:00 2001 From: Alistair Watts Date: Thu, 6 Feb 2025 10:11:51 +0000 Subject: [PATCH 4/9] Test now uses monkeypatch and checks error message text --- tests/test_jwe.py | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/tests/test_jwe.py b/tests/test_jwe.py index 8c5ff387..52d9cde3 100644 --- a/tests/test_jwe.py +++ b/tests/test_jwe.py @@ -527,33 +527,26 @@ def test_kid_header_not_present_when_not_provided(self): assert "kid" not in header @pytest.mark.skipif(AESKey is None, reason="No AES backend") - def test_jwe_with_excessive_data(self): + def test_jwe_with_excessive_data(self, monkeypatch): enc = ALGORITHMS.A256CBC_HS512 alg = ALGORITHMS.RSA_OAEP_256 - import jose.constants - old_limit = jose.constants.JWE_SIZE_LIMIT - try: - jose.constants.JWE_SIZE_LIMIT = 1024 - encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg) - header = json.loads(base64url_decode(encrypted.split(b".")[0])) - with pytest.raises(JWEError): - actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) - finally: - jose.constants.JWE_SIZE_LIMIT = old_limit + monkeypatch.setattr('jose.constants.JWE_SIZE_LIMIT', 1024) + encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg) + header = json.loads(base64url_decode(encrypted.split(b".")[0])) + with pytest.raises(JWEError) as excinfo: + actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) + assert 'JWE string' in str(excinfo.value) + assert 'bytes exceeds' in str(excinfo.value) @pytest.mark.skipif(AESKey is None, reason="No AES backend") - def test_jwe_zip_with_excessive_data(self): + def test_jwe_zip_with_excessive_data(self, monkeypatch): # Test that a fix for CVE-2024-33664 is in place. enc = ALGORITHMS.A256CBC_HS512 alg = ALGORITHMS.RSA_OAEP_256 - import jose.constants - old_limit = jose.constants.JWE_SIZE_LIMIT - try: - jose.constants.JWE_SIZE_LIMIT = 1024 - encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg, zip=ZIPS.DEF) - assert len(encrypted) < jose.constants.JWE_SIZE_LIMIT - header = json.loads(base64url_decode(encrypted.split(b".")[0])) - with pytest.raises(JWEError): - actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) - finally: - jose.constants.JWE_SIZE_LIMIT = old_limit + monkeypatch.setattr('jose.constants.JWE_SIZE_LIMIT', 1024) + encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg, zip=ZIPS.DEF) + assert len(encrypted) < jose.constants.JWE_SIZE_LIMIT + header = json.loads(base64url_decode(encrypted.split(b".")[0])) + with pytest.raises(JWEError) as excinfo: + actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) + assert 'Decompressed JWE string exceeds' in str(excinfo.value) From 367913aaa28e62dd8910c3110ba0f7b0b97b955d Mon Sep 17 00:00:00 2001 From: Asher Foa Date: Thu, 6 Feb 2025 09:54:14 -0500 Subject: [PATCH 5/9] Update .github/workflows/ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aee3dd9e..009e2317 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.10 + python-version: “3.10“ - name: Install dependencies run: | pip install -U setuptools From 6aa63566577318840e1161eaf0c70cb963429e64 Mon Sep 17 00:00:00 2001 From: Asher Foa Date: Thu, 6 Feb 2025 09:56:27 -0500 Subject: [PATCH 6/9] Update .github/workflows/ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 009e2317..b36e523b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: “3.10“ + python-version: "3.10" - name: Install dependencies run: | pip install -U setuptools From 8b1bf0d01139742be8c7f330468a48d724cb5bcb Mon Sep 17 00:00:00 2001 From: Asher Foa Date: Thu, 6 Feb 2025 14:42:12 -0500 Subject: [PATCH 7/9] Update jose/jwe.py --- jose/jwe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jose/jwe.py b/jose/jwe.py index 91343f3b..c1bb52b1 100644 --- a/jose/jwe.py +++ b/jose/jwe.py @@ -6,7 +6,7 @@ from . import jwk from .backends import get_random_bytes -from .constants import ALGORITHMS, ZIPS, JWE_SIZE_LIMIT +from .constants import ALGORITHMS, JWE_SIZE_LIMIT, ZIPS from .exceptions import JWEError, JWEParseError from .utils import base64url_decode, base64url_encode, ensure_binary From ff7c69405058d4ad13969b604bcab560bd246a1e Mon Sep 17 00:00:00 2001 From: Asher Foa Date: Thu, 6 Feb 2025 14:42:47 -0500 Subject: [PATCH 8/9] Update tests/test_jwe.py --- tests/test_jwe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_jwe.py b/tests/test_jwe.py index 52d9cde3..16b3e5e6 100644 --- a/tests/test_jwe.py +++ b/tests/test_jwe.py @@ -5,7 +5,7 @@ import jose.backends from jose import jwe from jose.constants import ALGORITHMS, ZIPS -from jose.exceptions import JWEParseError, JWEError +from jose.exceptions import JWEError, JWEParseError from jose.jwk import AESKey, RSAKey from jose.utils import base64url_decode From 046a250e25b9b838d50da05c056e0fa71d6dfa9c Mon Sep 17 00:00:00 2001 From: Asher Foa Date: Thu, 6 Feb 2025 14:48:41 -0500 Subject: [PATCH 9/9] fmt --- tests/test_jwe.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_jwe.py b/tests/test_jwe.py index 16b3e5e6..6ab99719 100644 --- a/tests/test_jwe.py +++ b/tests/test_jwe.py @@ -530,23 +530,23 @@ def test_kid_header_not_present_when_not_provided(self): def test_jwe_with_excessive_data(self, monkeypatch): enc = ALGORITHMS.A256CBC_HS512 alg = ALGORITHMS.RSA_OAEP_256 - monkeypatch.setattr('jose.constants.JWE_SIZE_LIMIT', 1024) - encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg) + monkeypatch.setattr("jose.constants.JWE_SIZE_LIMIT", 1024) + encrypted = jwe.encrypt(b"Text" * 64 * 1024, PUBLIC_KEY_PEM, enc, alg) header = json.loads(base64url_decode(encrypted.split(b".")[0])) with pytest.raises(JWEError) as excinfo: actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) - assert 'JWE string' in str(excinfo.value) - assert 'bytes exceeds' in str(excinfo.value) + assert "JWE string" in str(excinfo.value) + assert "bytes exceeds" in str(excinfo.value) @pytest.mark.skipif(AESKey is None, reason="No AES backend") def test_jwe_zip_with_excessive_data(self, monkeypatch): # Test that a fix for CVE-2024-33664 is in place. enc = ALGORITHMS.A256CBC_HS512 alg = ALGORITHMS.RSA_OAEP_256 - monkeypatch.setattr('jose.constants.JWE_SIZE_LIMIT', 1024) - encrypted = jwe.encrypt(b"Text"*64*1024, PUBLIC_KEY_PEM, enc, alg, zip=ZIPS.DEF) + monkeypatch.setattr("jose.constants.JWE_SIZE_LIMIT", 1024) + encrypted = jwe.encrypt(b"Text" * 64 * 1024, PUBLIC_KEY_PEM, enc, alg, zip=ZIPS.DEF) assert len(encrypted) < jose.constants.JWE_SIZE_LIMIT header = json.loads(base64url_decode(encrypted.split(b".")[0])) with pytest.raises(JWEError) as excinfo: actual = jwe.decrypt(encrypted, PRIVATE_KEY_PEM) - assert 'Decompressed JWE string exceeds' in str(excinfo.value) + assert "Decompressed JWE string exceeds" in str(excinfo.value)