VAITP Dataset

← Back to the dataset

CVE-2025-62708

High memory usage in pypdf via crafted PDF using LZWDecode filter.

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

pypdf is a free and open-source pure-python PDF library. Prior to version 6.1.3, an attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires parsing the content stream of a page using the LZWDecode filter. This has been fixed in pypdf version 6.1.3.

CVSS base score
6.6
Published
2025-10-22
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
pypdf
Fixed by upgrading
Yes

Solution

Upgrade to `pypdf>=6.1.3`.

Vulnerable code sample

import sys

# This code represents the vulnerable logic present in pypdf before version 6.1.3.
# The vulnerability (CVE-2025-62708, a conceptual ID for this example) lies in the
# LZWDecode filter, which can be exploited by a crafted PDF to cause a
# "decompression bomb". This means a small amount of compressed data expands
# to an extremely large amount of data, consuming excessive memory.

# A real-world LZW bomb would use clever dictionary manipulation to achieve
# exponential expansion. For this demonstration, we simulate the *result*
*of such an attack: an unbounded allocation of memory during decoding.

def LZWDecode_vulnerable(data):
    """
    A simplified and conceptual representation of the vulnerable LZWDecode function.
    It simulates the behavior of decoding a malicious stream that leads to
    massive memory allocation, without any checks on the output size.
    """
    # In a real exploit, the content of `data` would instruct the decoder
    # on how to create the bomb. Here, we hardcode the bomb's effect for clarity.
    # The vulnerability is that the library trusts the compressed stream's content.

    # This simulates the decoder being instructed to generate a multi-gigabyte output.
    # A value of 2 attempts to allocate 2GB of memory.
    gigabytes_to_allocate = 2
    target_output_size = gigabytes_to_allocate * (1024**3)

    # The vulnerable operation: creating a large data buffer without any safety limits.
    # In a real decoder, this would happen in a loop that appends decoded data.
    # This direct allocation represents the final outcome of that process.
    try:
        # We use a bytearray for a more realistic memory allocation pattern.
        # A real decoder would append byte by byte or chunk by chunk.
        # This loop simulates that incremental, yet unbounded, growth.
        decompressed_data = bytearray()
        i = 0
        while i < target_output_size:
            decompressed_data.append(ord('A'))
            i += 1
        return bytes(decompressed_data)
    except MemoryError:
        # This is the expected outcome on most systems: the process runs out of memory.
        sys.stderr.write("MemoryError: Failed to allocate memory, as expected from the vulnerability.\n")
        # In a real server or application, this could lead to a crash or DoS.
        raise


# --- Triggering the vulnerability ---

# This represents a small, malicious stream from a crafted PDF file.
# Its actual content is irrelevant in this simulation, as we have hardcoded
# the bomb's effect in the function above. In a real attack, these bytes
# would contain the LZW instructions to create the bomb.
malicious_lzw_compressed_data = b'\x80\x0b\xef\x01\x95...'

# This is the call that would happen inside pypdf when parsing a page
# content stream with the /LZWDecode filter.
# WARNING: Executing this line will attempt to allocate a large amount of
# memory (2 GB in this example) and will likely cause the program to crash
# or become unresponsive.
print("Attempting to decode a malicious LZW stream. This may cause a MemoryError.")
vulnerable_output = LZWDecode_vulnerable(malicious_lzw_compressed_data)

# This line will likely not be reached.
print(f"Vulnerability failed to trigger; allocated {len(vulnerable_output)} bytes.")

Patched code sample

import io

def fixed_lzw_decode(data: bytes, max_output_size: int = 100 * 1024 * 1024) -> bytes:
    """
    A simplified conceptual representation of the fixed LZW decoding algorithm
    in pypdf. This is not a complete or correct LZW implementation, but serves
    to demonstrate the specific vulnerability fix.

    The vulnerability (similar to CVE-2023-26477) allowed a crafted data stream
    to cause excessive memory allocation during decoding. The fix introduces a
    check on the output size within the decoding loop.
    """
    # LZW control codes used for demonstration
    CLEAR_TABLE = 256
    END_OF_DATA = 257

    # Simplified stream and table setup for the decoding process
    stream = io.BytesIO(data)
    table = {i: bytes([i]) for i in range(256)}
    table_size = 258
    decoded_data = bytearray()

    # Simplified logic to read codes from the stream. A real implementation
    # reads variable-bit-length codes.
    def read_code() -> int:
        byte = stream.read(1)
        return byte[0] if byte else END_OF_DATA

    code = read_code()
    last_entry = b""

    while code != END_OF_DATA:
        # --- START OF THE VULNERABILITY FIX ---
        # Before processing the next code, the patched version checks if the
        # output size has grown beyond a reasonable limit. This prevents a
        # Denial-of-Service attack where a crafted stream (e.g., one that
        # repeatedly uses CLEAR_TABLE) generates an extremely large output,
        # exhausting system memory.
        if len(decoded_data) > max_output_size:
            raise MemoryError(
                f"LZW decoded data exceeds size limit of {max_output_size} bytes"
            )
        # --- END OF THE VULNERABILITY FIX ---

        if code == CLEAR_TABLE:
            # Resetting the table is a key part of the exploit mechanism,
            # as it allows an attacker to prevent the dictionary from filling
            # up, thus continuing to produce output.
            table = {i: bytes([i]) for i in range(256)}
            table_size = 258
            code = read_code()
            if code == END_OF_DATA:
                break
            entry = table.get(code, b"")
            decoded_data.extend(entry)
            last_entry = entry
        elif code in table:
            entry = table[code]
            decoded_data.extend(entry)
            if last_entry and table_size < 4096:
                table[table_size] = last_entry + entry[:1]
                table_size += 1
            last_entry = entry
        else:
            # Handle the 'code not in table' case for LZW
            if not last_entry:
                raise ValueError("Invalid LZW stream: code not in table")
            entry = last_entry + last_entry[:1]
            decoded_data.extend(entry)
            if table_size < 4096:
                table[table_size] = entry
                table_size += 1
            last_entry = entry

        code = read_code()

    return bytes(decoded_data)

Payload

%PDF-1.7
%µ¶±

1 0 obj
<</Type/Catalog/Pages 2 0 R>>
endobj

2 0 obj
<</Type/Pages/Count 1/Kids[3 0 R]>>
endobj

3 0 obj
<</Type/Page/Parent 2 0 R/MediaBox[0 0 1 1]/Contents 4 0 R>>
endobj

4 0 obj
<</Filter/LZWDecode/Length 22>>
stream
€
endstream
endobj

xref
0 5
0000000000 65535 f 
0000000015 00000 n 
0000000062 00000 n 
0000000117 00000 n 
0000000203 00000 n 

trailer
<</Size 5/Root 1 0 R>>
startxref
297
%%EOF

Cite this entry

@misc{vaitp:cve202562708,
  title        = {{High memory usage in pypdf via crafted PDF using LZWDecode filter.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-62708},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-62708/}}
}
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 ::