CVE-2026-96639: Pillow multi-frame loaders can reuse stale native image storage after mode transitions
Pillow 11.0.0 up to 13.0.0 can reuse stale native image storage when a multi-frame file changes mode between frames. A crafted MPO, PSD, DCX or MIC file causes out-of-bounds writes and crashes, and the reporter demonstrated command execution through the MPO path. Upgrade to Pillow 13.0.0.
Published Oct 22, 2026 · Analysis Oct 22, 2026
What an attacker can do
ImageFile.load_prepare() reallocates native ImagingCore storage only when _im is None. It never checks that retained storage still matches self.mode and self.size. Several seek() and _open() paths change the Python-side mode and decoder tiles for a later frame without clearing _im. The decoder then writes wide pixels into narrow storage.
In a crafted two-frame MPO, frame 0 allocates mode L storage and frame 1, same dimensions, decodes CMYK or RGB into it. The documented ImageSequence.Iterator plus frame.load() path reaches this. PSD (PsdImageFile.seek()), DCX (DcxImageFile.seek() calling PcxImageFile._open()) and MIC (via TiffImageFile._open() and a NextIFD transition) are also affected. TIFF's own seek() checks mode and size, so it serves as a control.
The attacker only needs the target to open and iterate a file they supply. The reporter's demo was an unauthenticated upload service that processes MPO files with Pillow; a fixed harmless marker command ran 10 of 10 times on Pillow 12.3.0, against 0 of 10 for the control. Consequences are denial of service, memory corruption and, in matching runtime profiles, code execution. Services that thumbnail or convert user uploads are the exposed class.
Who is affected
Pillow from 11.0.0 up to but not including 13.0.0. The reporter tested 12.3.0. The risk applies when your code opens untrusted multi-frame images (MPO, PSD, DCX, MIC) and loads more than the first frame, for instance by iterating with ImageSequence.Iterator.
pip show PillowCheck images, base containers and virtual environments of upload-handling services separately.
Pillow
- Ecosystem
- PyPI
- Affected
- >= 11.0.0, < 13.0.0
- Fixed in
- 13.0.0
- Check
pip show Pillow
How to fix it
Upgrade to Pillow 13.0.0:
pip install --upgrade "Pillow>=13.0.0"The fix is pull request 10105, "Clear core image if mode or size has changed during seek()". The release notes show no backport to 12.x, so a 12.x pin stays vulnerable. If you cannot upgrade yet, restrict accepted formats with Image.open(..., formats=[...]) to exclude MPO, PSD, DCX and MIC, which is a general Pillow feature rather than a step from the advisory.
How to tell if you were hit
Look for worker processes that crash with segmentation faults or memory corruption errors while handling uploaded images, especially after uploads of MPO, PSD, DCX or MIC files. Check upload storage for multi-frame files whose frames have different modes. Look for unexpected child processes spawned by image-processing workers and for outbound connections from them. Inventory dependencies with pip show Pillow across your services.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
Summary
Pillow multi-frame image loaders can retain native ImagingCore storage allocated for one frame while changing the Python-side image mode and decoder tiles for a later frame.
If frame 0 allocates storage for a narrow mode such as L, and a later frame of the same dimensions switches to a wider mode such as RGB or CMYK, Pillow may decode wider pixels into the stale narrow native buffer. This causes native out-of-bounds writes and process crashes. In the MPO/JPEG path, I also demonstrated command execution in matching Python/Pillow runtime profiles through Pillow's documented ImageSequence.Iterator + frame.load() path.
Details
The root cause appears to be that ImageFile.load_prepare() only reallocates native storage when _im is None. It does not verify that the retained native ImagingCore storage still matches self.mode and self.size.
Several multi-frame seek() / _open() paths can update self._mode and decoder tiles for a later frame without invalidating _im. The decoder then writes or reads according to the new frame mode while the native storage still belongs to the old frame mode.
Verified affected surfaces:
- PSD
PsdImageFile.seek() can switch from grayscale/native L storage to RGB layer decoding while retaining _im. This produces attacker-controlled native out-of-bounds writes and reliable crashes.
- MPO/JPEG
A crafted two-frame MPO can make frame 0 allocate L storage, then make frame 1 decode CMYK / RGB data into the retained L storage. The documented ImageSequence.Iterator path reaches this with frame.load(). Verified native memory corruption, crash, over-read behavior, and scoped harmless-marker command execution on Pillow 12.3.0.
- DCX
DcxImageFile.seek() calls PcxImageFile._open(), which changes mode/tile state without a native-storage mode guard. A crafted DCX with L then RGB PCX frames produces retained-storage mismatch and crash.
- MIC
MIC can call TiffImageFile._open() directly and bypass TIFF's normal guarded seek() behavior. With a NextIFD-based reachable frame transition, this produces retained-storage mismatch and crash.
TIFF appears to be a useful positive control because its seek() path checks both size and mode before reusing storage.
Impact
- process crash / denial of service,
- native out-of-bounds write,
- native over-read in the MPO path.
Stronger proven impact:
- command execution in matching Python/Pillow runtime profiles through the MPO/JPEG path.
Server-side deployment impact:
- Create a local vulnerable web upload service that accepts an unauthenticated MPO upload and automatically processes it with Pillow.
- The attack is one HTTP request and requires no user interaction after upload.
- The fixed harmless command-output marker succeeded 10/10 times.
- The
_im=Nonecontrol was clean 10/10 times.
Reproduction Evidence
Tested version:
Pillow 12.3.0MPO terminal proof environments:
Python 3.12.3 + Pillow 12.3.0, clean PyPI environment:
marker 10/10, control 0/10
Python 3.13.14 + Pillow 12.3.0, prepared profile:
marker 10/10, control 0/10Server-side web-upload proof:
control 1..10: MARKER_MISSING
control_summary ok 0 missing 10 /10
vulnerable 1..10: MARKER_OK marker_output <server-user>
vulnerable_summary ok 10 missing 0 /10The terminal proof uses a fixed harmless marker command only. It does not use a reverse shell, persistence, data exfiltration, or arbitrary attacker-selected commands.
I can provide the following private artifacts to maintainers:
- minimal crash fixtures for PSD, MPO, DCX, and MIC,
- non-weaponized MPO marker PoC,
- local vulnerable web upload demo,
- gdb/GEF geometry receipts,
- control-run logs,
- proposed regression-test shape.
Minimal Reproduction Shape
For the MPO path, the vulnerable consumer shape is:
from io import BytesIO
from PIL import Image, ImageSequence
source = BytesIO(open(path, "rb").read())
image = Image.open(source)
iterator = ImageSequence.Iterator(image)
next(iterator).load()
next(iterator).load()The crafted image shape is:
frame 0: mode L, dimensions W x H
frame 1: same W x H, wider mode such as CMYKFrame 0 allocates native L storage. Frame 1 changes the logical mode and decoder state but can reuse the old native storage. The frame 1 decoder writes using the wider mode into the narrow storage.
Suggested Fix Direction
Before reusing native ImagingCore storage, Pillow should verify that the retained storage is compatible with the current logical image mode and size.
A central guard in ImageFile.load_prepare() could compare native storage mode/size against self.mode / self.size and allocate fresh storage when they differ. Alternatively, every mode-changing seek() / _open() path should explicitly invalidate _im before decoding.
Caution: TIFF has specialized tile-size behavior and already has a mode/size guard, so a central fix should avoid regressing TIFF tile-buffer handling.
Credit
Rotem KamaMethodology Note
AI-assisted analysis was used for discovery and verification support. Claims in this report are limited to the local artifacts, controls, and command outputs described above.
Credited to rotemkama.
Questions
- Is Pillow 12.3.0 vulnerable?
Yes. The reporter built the proof on 12.3.0, and every release from 11.0.0 to below 13.0.0 is listed as affected.
- Does this need user interaction?
No. The scenario is a service that processes uploaded images automatically, so one HTTP request with a crafted MPO is enough.
- Which formats are involved?
The advisory verifies PSD, MPO/JPEG, DCX and MIC. Only MPO has a demonstrated command-execution path.
- Is it exploited in the wild?
No source reports that. The reporter provided a harmless-marker proof of concept and withheld weaponized material.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
More PyPI vulnerabilities
All PyPI entriesCVE-2026-112393
Pillow: Heap buffer overflow in _tiffWriteProc when saving TIFF to BytesIO
critical9.2Oct 21, 2026CVE-2026-112900
Pillow: Argument Injection in Ghostscript call, potentially leading into RCE
critical9.2Oct 21, 2026CVE-2026-112403
Pillow: Heap buffer overflow in ImagingJpegDecode
critical9.2Oct 20, 2026CVE-2023-50447
Arbitrary Code Execution in Pillow
critical8.1Jan 19, 2024
