CVE Patch Review

CVE-2026-59938: Root-Cause Guard for pypdf Image XObject OOM

CVE-2026-59938 · Updated 2026-07-23 Root-cause

Summary

The patch adds an explicit pre-allocation size check in pypdf image XObject parsing, rejecting images whose declared dimensions imply a decoded buffer larger than the configured limit. This directly addresses the uncontrolled memory allocation vector described in the CVE by validating Width/Height-derived allocation size before processing. The implementation is effective, but naming/documentation remain inconsistent because the code still uses FLATE_MAX_BUFFER_SIZE while docs refer to IMAGE_MAX_BUFFER_SIZE.

Analysis

Vulnerability

CVE-2026-59938 describes an uncontrolled memory allocation issue in pypdf image parsing where attacker-controlled PDF XObject metadata can declare extremely large /Width and /Height values. The vulnerable path computed pixel count from image dimensions and used it in downstream image reconstruction logic without first enforcing a hard upper bound on the implied decoded image buffer size. As summarized by the CVE record and NVD entry, a small malicious PDF could therefore trigger excessive allocation and process termination via OOM.

The relevant pre-patch logic in pypdf/generic/_image_xobject.py operated on the raw dimension product:

nb_pix = size[0] * size[1]
if data_length % nb_pix != 0:
k = nb_pix * len(mode) / data_length

This is dangerous because the allocation pressure is driven by decoded image size, not by compressed PDF size. If size[0] * size[1] * bytes_per_pixel is attacker-inflated, the parser can be coerced into attempting an enormous in-memory image representation.

Patch

The fix in the upstream commit c64583be16b8e8763d8777075f8ecbf382014b7a and associated pull request #3888 adds a decoded-buffer bound check before image construction. The patched code derives the required byte count from dimensions and mode, imports the configured limit, and raises LimitReachedError when the request exceeds that threshold:

pixel_count = size[0] * size[1]
bytes_per_pixel = len(mode)
required_byte_count = pixel_count * bytes_per_pixel

from pypdf.filters import FLATE_MAX_BUFFER_SIZE  # noqa: PLC0415
if required_byte_count > FLATE_MAX_BUFFER_SIZE:
    raise LimitReachedError(
        f"Requested image buffer size {required_byte_count} exceeds limit {FLATE_MAX_BUFFER_SIZE}."
    )

if data_length % pixel_count != 0:
    ...
k = required_byte_count / data_length

This is the correct control point for the reported issue: it validates the memory requirement implied by image metadata before the parser proceeds. The patch also adds regression coverage in tests/generic/test_image_xobject.py for oversized images across multiple modes (1, RGB, CMYK) and adjusts a filter test to avoid interference from the new image-size guard by patching FLATE_MAX_BUFFER_SIZE to sys.maxsize.

One inconsistency remains in naming and documentation. The security documentation now refers to pypdf.filters.IMAGE_MAX_BUFFER_SIZE, but the code still exposes and uses FLATE_MAX_BUFFER_SIZE with an inline TODO noting the mismatch. That does not negate the security fix, but it is an API/documentation correctness issue visible in the same patch set.

Review

Pros

  • The patch addresses the actual root cause of the OOM vector: unbounded decoded image buffer sizing derived from attacker-controlled dimensions.
  • The check is placed before expensive image processing, so it prevents allocation rather than merely handling failure after memory pressure occurs.
  • The bound is computed using semantically relevant inputs: pixel count multiplied by bytes per pixel.
  • The implementation fails closed with a specific LimitReachedError, which is preferable to allowing allocator-driven crashes.
  • Regression tests cover multiple image modes and assert the exact rejection path, improving confidence that the guard is intentional and stable.
  • The patch is source-aligned with the vulnerability description in the NVD and CVE records.

Cons

  • The limit constant naming is inconsistent: docs say IMAGE_MAX_BUFFER_SIZE, while code still uses FLATE_MAX_BUFFER_SIZE and explicitly marks that as a TODO in pypdf/filters.py.
  • The patch summary does not show any validation for pathological numeric edge cases beyond the size threshold, such as malformed zero or negative dimensions; those may already be handled elsewhere, but they are not evidenced in the provided diff.
  • The guard is specific to this image construction path. Based on the provided snippets alone, it is not possible to conclude whether all image-related allocation paths in pypdf are normalized through the same check.
  • The configured default remains a global fixed threshold of 75 MB, which is pragmatic but may be too high or too low depending on deployment constraints.

Verdict

Root-cause.

The patch directly constrains the attacker-controlled allocation primitive by bounding required_byte_count before image materialization. That is the essential missing validation for this CVE, and it converts an OOM crash condition into a deterministic parser-side rejection. The remaining issue is mostly polish: the documentation/API name mismatch around IMAGE_MAX_BUFFER_SIZE versus FLATE_MAX_BUFFER_SIZE should be cleaned up to avoid operator confusion, but it does not materially weaken the security remediation shown in the commit and PR.

Sources