Jul 23, 2026·8 min read·41 visits
Uncontrolled memory allocation in pypdf < 6.14.0 allows remote attackers to cause a Denial of Service (OOM crash) by submitting a small PDF with artificially large image dimensions (/Width and /Height) in XObject metadata.
An uncontrolled memory allocation vulnerability (CWE-789) exists in pypdf prior to version 6.14.0. The library blindly trusted user-controlled image dimensions (/Width and /Height) from PDF metadata, allowing attackers to trigger physical memory exhaustion and an Out-of-Memory crash via tiny, malformed files.
The open-source Python library pypdf is widely utilized for reading, manipulating, and writing PDF files across various server-side applications. Within this library, the image extraction and decoding workflows parse specialized image dictionary structures, such as Image XObjects and inline images. The processing of these image objects relies on extracting metadata descriptors from the PDF file to construct native python representations of image formats.
Prior to version 6.14.0, the library did not validate the sanity of declared dimensions prior to initiating fallback processing for mismatched data. When parsing a PDF, an application resolving images relies on the /Width and /Height keys specified within the image object's dictionary to allocate buffers and read the physical stream data. This setup exposes an attack surface where maliciously crafted, structurally abnormal PDF files are processed directly on host systems.
This flaw is classified under CWE-789 (Memory Allocation with Excessive Size Value). When a host application processes an untrusted PDF, a mismatched length configuration triggers a byte-expansion routine that attempts to construct an excessively large string in system memory. This uncontrolled allocation triggers immediate physical memory depletion and process termination.
The root cause of CVE-2026-59938 resides in the _image_from_bytes function in pypdf/generic/_image_xobject.py. This helper function accepts physical stream data alongside untrusted dimensions derived from the PDF's /Width and /Height dictionary entries. The function's main execution flow attempts to leverage the Python Imaging Library (Pillow) wrapper Image.frombytes(mode, size, data) to map raw bytes into an image object.
When a discrepancy exists between the expected dimensions and the actual uncompressed length of the data stream, Pillow raises a ValueError. The library handles this mismatch by entering a fallback recovery block. In this block, pypdf attempts to dynamically reconstruct the buffer to align with the metadata. It calculates a scaling factor k based on the formula:
k = (pixel_count * bytes_per_pixel) / data_length
Here, pixel_count is the product of /Width and /Height, and bytes_per_pixel is determined by the length of the string representation of the color mode (e.g., 3 bytes for 'RGB'). The code then uses a list comprehension coupled with b"".join() to pad the mismatched stream to the expected size:
data = b"".join(bytes((x,) * int(k)) for x in data)
Because the factor k is derived from metadata that is completely controlled by the user, an attacker can define extremely large dimensions (e.g., 100,000 by 80,000) while providing only a single byte of actual physical payload data. This mismatch forces k to scale into tens of billions, causing the subsequent join() execution to attempt an unconstrained allocation of gigabytes of RAM. The Python interpreter terminates abruptly due to memory exhaustion.
To fully understand the changes implemented to address CVE-2026-59938, we examine the differences between the vulnerable code path and the secure implementation introduced in version 6.14.0.
The vulnerable execution flow directly passed the untrusted size and mode attributes into the error handler where allocation occurred. Below is the vulnerable implementation of the parsing function:
# Vulnerable implementation in pypdf < 6.14.0
def _image_from_bytes(
mode: str, size: tuple[int, int], data: bytes
) -> Image.Image:
try:
img = Image.frombytes(mode, size, data)
except ValueError as exc:
nb_pix = size[0] * size[1]
data_length = len(data)
if data_length == 0:
raise EmptyImageDataError(
"Data is 0 bytes, cannot process an image from empty data."
) from exc
if data_length % nb_pix != 0:
raise
k = nb_pix * len(mode) / data_length
# Unbounded multiplication and buffer creation occurs here
data = b"".join(bytes((x,) * int(k)) for x in data)
img = Image.frombytes(mode, size, data)
return imgThe patch merged in commit c64583be16b8e8763d8777075f8ecbf382014b7a introduces a strict safety check before executing Pillow commands or evaluating fallback code. It calculates the memory footprint mathematically and halts execution if the size exceeds a predefined threshold:
# Patched implementation in pypdf >= 6.14.0
def _image_from_bytes(
mode: str, size: tuple[int, int], data: bytes
) -> Image.Image:
# Mathematically calculate the total pixel count and expected buffer size
pixel_count = size[0] * size[1]
bytes_per_pixel = len(mode)
required_byte_count = pixel_count * bytes_per_pixel
# Enforce upper bound limits before performing operations
from pypdf.filters import FLATE_MAX_BUFFER_SIZE
if required_byte_count > FLATE_MAX_BUFFER_SIZE:
raise LimitReachedError(
f"Requested image buffer size {required_byte_count} exceeds limit {FLATE_MAX_BUFFER_SIZE}."
)
try:
img = Image.frombytes(mode, size, data)
except ValueError as exc:
data_length = len(data)
if data_length == 0:
raise EmptyImageDataError(
"Data is 0 bytes, cannot process an image from empty data."
) from exc
if data_length % pixel_count != 0:
raise
k = required_byte_count / data_length
data = b"".join(bytes((x,) * int(k)) for x in data)
img = Image.frombytes(mode, size, data)
return imgThis defensive design is highly effective because it computes the total buffer requirement first, using Python's native arbitrary-precision integers which are immune to traditional overflow exploits. The security threshold FLATE_MAX_BUFFER_SIZE defaults to 75 MB, which provides a sensible default upper bound for typical image operations. However, systems processing multiple pages concurrently must ensure that cumulative concurrent allocations do not bypass host-level memory constraints.
An attack targeting CVE-2026-59938 requires transmitting a malformed PDF document to a server running a vulnerable version of pypdf. The exploitation does not require authentication or specific configurations beyond the target application processing user-supplied PDFs and requesting image resolution or content extraction.
The vector relies on defining an Image XObject dictionary containing structural metadata that triggers the library's fallback path. A conceptual representation of this XObject inside the PDF content stream is organized as follows:
5 0 obj
<<
/Type /XObject
/Subtype /Image
/Width 100000
/Height 80000
/ColorSpace /DeviceRGB
/BitsPerComponent 8
/Length 1
>>
stream
\x00
endstream
endobj
This small block defines an image footprint demanding 24 GB of uncompressed RAM (100,000 * 80,000 * 3 bytes). The underlying stream contains only a single byte. When a script iterates over the page's resources or attempts to convert pages using page.images, pypdf parses this stream and activates the byte-expansion loop.
The execution flow of this vulnerability is visualized in the following system flow diagram:
To demonstrate this behavior, a test runner executing list(page.images) on the malformed PDF payload immediately triggers high CPU thrashing and spikes system memory utilization to maximum capacity before causing the operating system kernel to issue a SIGKILL (exit code 137).
The primary impact of CVE-2026-59938 is local and remote Denial of Service (DoS). When exploited, the vulnerability leads to resource exhaustion and terminates the affected process. This behavior represents a significant threat to backend architectures that process document uploads automatically, such as resume screening tools, financial auditing systems, or cloud storage viewers.
The CVSS v3.1 base score is 5.3, with an availability impact score of 'Low' under standard mapping because the crash typically terminates only the parsing thread or container, which can be automatically restarted. However, in environments lacking container orchestration or process monitoring, this crash results in permanent application downtime until manual administrator intervention occurs.
From a data security perspective, the vulnerability does not present a mechanism for remote code execution, file modification, or unauthorized data access. The attack complexity is low, and no specialized preconditions or user interaction are required, making it highly attractive for adversaries seeking to degrade application availability with minimal payload sizes.
The standard remediation for this vulnerability is upgrading pypdf to version 6.14.0 or later. This release addresses the uncontrolled buffer allocation by introducing bounded limits. Upgrades can be performed natively using python package managers.
For installations where immediate package upgrades are not feasible, temporary workarounds can mitigate the risk. Administrators should deploy sandbox environments utilizing operating system controls to constrain resource limits. For example, configuring limits via the Linux resource API or setting container memory boundaries ensures that an OOM crash inside pypdf is isolated and does not disrupt neighboring system processes.
# Restricting process virtual memory limit using Python's resource module
import resource
def limit_memory(max_memory_bytes):
resource.setrlimit(resource.RLIMIT_AS, (max_memory_bytes, max_memory_bytes))In addition, modern Web Application Firewalls (WAF) or pre-processing pipeline checks can analyze incoming PDF byte patterns. Filtering for incoming documents that describe a massive disparity between declared pixel footprints and actual stream lengths provides a robust layer of defense before the PDF reaches the processing library.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
pypdf py-pdf | < 6.14.0 | 6.14.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-789 |
| Attack Vector | Network |
| CVSS Score | 5.3 (CVSS v3.1) |
| EPSS Score | 0.00303 (Percentile: 22.45%) |
| Impact | Denial of Service (OOM Application Crash) |
| Exploit Status | Proof-of-Concept (PoC) available |
| KEV Status | Not Listed |
The product allocates memory based on an untrusted, user-controlled size value without adequately validating that the size is within reasonable limits, making it susceptible to resource exhaustion.
A path traversal vulnerability (CWE-22) in Contao CMS allows unauthenticated remote attackers to bypass directory boundary restrictions in ImagesController and access files within the project directory.
Contao Open Source CMS versions 4.0.0 through 5.3.49 and 5.4.0-RC1 through 5.7.11 contain a Cross-Site Request Forgery (CSRF) vulnerability in backend parameter handling. The `RequestTokenListener` component validates anti-CSRF tokens solely for HTTP POST requests, while GET-based declarative guards run only when an `act` parameter is present in the query string. Consequently, custom backend actions dispatched via alternative parameters such as `key=` can execute without CSRF token verification when triggered by an authenticated user.
Contao CMS versions 4.1.0 through 5.3.49 and 5.4.0-RC1 through 5.7.11 fail to validate form submission tokens and enforce rate limiting when processing activation email resend requests via HTTP POST, enabling resource exhaustion and account state enumeration.
An information disclosure vulnerability in Contao CMS allows unauthenticated site visitors to view protected page titles, URLs, and text excerpts through search queries when protected page indexing is disabled after previously being enabled.
In Vikunja prior to version 2.6.0, relation creation via the CalDAV endpoint fails to invoke the TaskRelation.CanCreate authorization check. This missing access control allows an authenticated user to establish unauthorized relationships and perform write operations against any task, provided its unique identifier (UID) is known.
A cross-project information disclosure vulnerability in Vikunja allows authenticated users with read access to one project to view private task details from unauthorized projects via subtask expansion parameters.