CVE-2026-96636
mediumpriority · track · 0EPSS 0.004 · 28 %ileUSEPSS · scored 2026-10-27
Pillow is a Python imaging library. Prior to 13.0.0, MpoImageFile.seek() and DcxImageFile.seek() reparse later MPO or DCX frames and replace self._size without rerunning Image._decompression_bomb_check(). A crafted multi-frame image can use a tiny first frame to pass Image.open() and then declare an over-limit later frame. Normal frame iteration through seek(), load(), or ImageSequence.Iterator can decode that frame and commit hundreds of megabytes or more, causing memory-exhaustion denial of service. This issue is fixed in version 13.0.0.
cvss
Technical severity, 0–10. One row per scoring source.
base score 0–10 · higher = more severe · hover a vector to decode
weaknesses
The CWE weakness classes this flaw belongs to.
affected products (2)
Products and version ranges known to be affected.
advisories (1)
Vendor and ecosystem advisories describing this.
- GHSA-p8hx-v2p6-rp84Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)2026-10-22ghsa · osvUS
### 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 ```
sources
Which sources assert this record, and when.
references (4) nvd
External citations — patches, advisories, analysis.
- https://github.com/python-pillow/Pillow/commit/f1acfc54c53502948d0d14121872061df931db48Patch
- https://github.com/python-pillow/Pillow/pull/10011Issue Tracking
- https://github.com/python-pillow/Pillow/releases/tag/13.0.0Release Notes
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-p8hx-v2p6-rp84Vendor Advisory
Attackers weaponise vulnerabilities like this against internet-exposed services. See what is exposed on your perimeter with a free passive scan.
Free scan