GitHub Advisoryadvisory · PyPI

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

MEDIUM Published 2026-10-22 Modified 2026-10-22 Official page ↗
Aliases
CVE-2026-96636
Ecosystem
PyPI
Packages
Pillow
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
Weaknesses
CWE-400, CWE-409
Reviewed
2026-10-22

Affected packages

• PyPI Pillow: < 13.0.0 → fixed in 13.0.0

Description

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 ```

Related

Alias · CVE (1)

Weakness · CWE (2)

  • CWE-400 Uncontrolled Resource Consumption weakness
  • CWE-409 Improper Handling of Highly Compressed Data (Data Amplification) weakness

References