---
id: "CVE-2026-96636"
kind: "vulnerability"
aliases: ["CVE-2026-96636", "GHSA-p8hx-v2p6-rp84"]
title: "Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)"
severity: "medium"
cvss: 6.5
epss: 0.00279999990016222
exploited: false
status: "open"
category: "package"
products: [{"name": "Pillow", "fixed": ["13.0.0"], "affected": "< 13.0.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-400", "cwe-409", "image-processing", "package", "pillow", "python", "self-hosted"]
not_affected_if: ["no-multi-frame-iteration", "no-untrusted-image-input", "pillow-13.0.0-or-later"]
published_at: "2026-10-21T21:20:31+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:29+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-p8hx-v2p6-rp84", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-96636", "first_seen_at": "2026-10-23T21:57:29+00:00"}]
---

# Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)

Pillow's decompression-bomb check only runs on the first frame of MPO and DCX images, so seek() to a later frame skips it. A crafted file with a tiny first frame and a huge later frame can make the process allocate very large amounts of memory, causing denial of service (out-of-memory).

_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 pip show pillow and check whether the version is below 13.0.0.
Search the code for Image.open calls followed by seek() on untrusted MPO or DCX files.

## Fix

Upgrade Pillow to 13.0.0 or later (pip install -U pillow).
Until then, do not iterate frames of untrusted MPO or DCX files, or reject them before opening.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-p8hx-v2p6-rp84
- 2026-10-23T21:57:29+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-96636

## From the sources

### Summary
Pillow's decompression-bomb protection (`Image._decompression_bomb_check`, gated by `Image.MAX_IMAGE_PIXELS`) is enforced only on the **first frame**, inside `Image.open()`. For multi-frame formats, `seek()` re-parses the target frame's header and resets `self._size`. `GifImagePlugin` and `TiffImagePlugin` re-run the bomb check on `seek()`, but **`MpoImageFile.seek()` and `DcxImageFile.seek()` do not.** A crafted MPO or DCX whose first frame is tiny (passing the check) but whose later frame declares enormous dimensions therefore bypasses the protection: loading that frame decodes an over-limit image and commits large amounts of memory — a denial of service (memory exhaustion / OOM).

Same class as the recently assigned CVE-2026-55379 (BdfFontFile), CVE-2026-55380 (GdImageFile) and CVE-2026-40192 (FITS GZIP).

### Details
`Image.open()` → `_open_core()` runs `_decompression_bomb_check(im.size)` once, on the frame present at open time. Both vulnerable `seek()` methods re-parse a new frame and set a new `self._size` without re-checking:

- **`PIL/MpoImagePlugin.py`, `MpoImageFile.seek()`**: calls `JpegImagePlugin.JpegImageFile._open(self)` (which sets `self._size` from the frame's JPEG SOF) and rebuilds `self.tile`, with no `Image._decompression_bomb_check(self.size)`.
- **`PIL/DcxImagePlugin.py`, `DcxImageFile.seek()`**: calls `PcxImageFile._open(self)` (which sets `self._size` from the page's PCX header), with no check.

Confirmed present in **12.3.0** (latest release) and on **`main`** (13.0.0.dev0). Compare `GifImagePlugin._seek` and `TiffImagePlugin`, which call `_decompression_bomb_check` correctly on seek.

### PoC
Self-contained; the victim uses **default** `MAX_IMAGE_PIXELS`:

```python
import io
from PIL import Image

# attacker builds the file (guard disabled only to CREATE the oversized frame)
Image.MAX_IMAGE_PIXELS = None
small = Image.new("RGB", (8, 8))
big   = Image.new("RGB", (14000, 14000), (120, 60, 30))   # 196,000,000 px > error threshold
buf = io.BytesIO()
small.save(buf, format="MPO", save_all=True, append_images=[big])
data = buf.getvalue()                                      # ~3 MB
Image.MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3) # restore default

# victim (default limits)
im = Image.open(io.BytesIO(data))   # frame 0 (8x8) passes the bomb check
im.seek(1)                          # frame 1 (14000x14000) -> NOT re-checked  <-- bug
im.load()                           # decodes the over-limit image, commits memory
print("loaded over-limit frame:", im.size)
```

Measured on Pillow 12.3.0 (default limit 89,478,485 px; error threshold 178,956,970 px):

| Vector | File size | Frame 1 | Peak RSS |
|---|---|---|---|
| MPO (JPEG) | ~3.0 MB | 14000×14000 (196 Mpx) | ~763 MB |
| DCX (PCX) | ~6.2 MB | 14000×14000 (196 Mpx) | ~200 MB |
| Control: same image standalone | — | 196 Mpx | `DecompressionBombError` (correctly blocked) |

The DCX vector (`0x3ADE68B1` magic + offset directory + two PCX pages) behaves identically via `DcxImageFile.seek()`. Scaling frame 1 toward JPEG's ~65500×65500 maximum drives multi-GB commitments from a small file, reliably OOM-killing the process.

### Impact
Denial of service (memory exhaustion / OOM) — CWE-400 / CWE-1284, a bypass of Pillow's documented decompression-bomb mitigation. `seek()` + `load()` is the normal way to iterate frames (e.g. `PIL.ImageSequence.Iterator`, thumbnailers, converters, 3D-photo handlers). Because **MPO is auto-detected from a JPEG carrying MPF metadata**, a file served with a `.jpg` extension can be opened as MPO, so any application that iterates frames of untrusted images is exposed.

### Suggested fix
Re-run the check after `seek()` re-parses a frame, mirroring GIF/TIFF:

```python
# PIL/MpoImagePlugin.py — end of MpoImageFile.seek()
self.tile = [ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
Image._decompression_bomb_check(self.size)   # add
self.__frame = frame

# PIL/DcxImagePlugin.py — end of DcxImageFile.seek()
PcxImageFile._open(self)
Image._decompression_bomb_check(self.size)   # add
```

---

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