diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 049a753..b36e523 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 diff --git a/jose/constants.py b/jose/constants.py index ab4d74d..58787d4 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 2c387ff..c1bb52b 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, JWE_SIZE_LIMIT, ZIPS 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 {len(jwe_str)} bytes 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 f089d56..6ab9971 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 JWEError, JWEParseError from jose.jwk import AESKey, RSAKey from jose.utils import base64url_decode @@ -525,3 +525,28 @@ 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, 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) + 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, 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) + 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)