CVE-2026-96636: Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)
Summary Pillow's decompression-bomb protection (Image.decompressionbombcheck, gated by Image.MAXIMAGEPIXELS) 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() → opencore() runs decompressionbombcheck(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.decompressionbombcheck(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 decompressionbombcheck correctly on seek.
PoC Self-contained; the victim uses default MAXIMAGEPIXELS:
python import io from PIL import Image
attacker builds the file (guard disabled only to CREATE the oversized frame) Image.MAXIMAGEPIXELS = 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", saveall=True, appendimages=[big]) data = buf.getvalue() # ~3 MB Image.MAXIMAGEPIXELS = 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.decompressionbombcheck(self.size) # add self.frame = frame
PIL/DcxImagePlugin.py — end of DcxImageFile.seek() PcxImageFile.open(self) Image.decompressionbombcheck(self.size) # add
Other sources
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.decompressionbombcheck(). 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.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/Pillowto a version that resolves this vulnerability.Fixed in 13.0.0 - Upgrade
Upgrade
bitnami/pillowto a version that resolves this vulnerability.Fixed in 13.0.0 - Upgrade
Upgrade
pip/pillowto a version that resolves this vulnerability.Fixed in 13.0.0 - Upgrade
Upgrade
debian/pillowto a version that resolves this vulnerability.Fixed in 13.0.0-1 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 13.0.0
Event History
Frequently Asked Questions
Which applications are exposed to this issue?
Applications using Pillow before 13.0.0 are exposed if they open attacker-controlled MPO or DCX files and subsequently access later frames, including through seek(), load(), or ImageSequence.Iterator. Processing only the initial frame does not describe the vulnerable path.
What must an attacker provide to trigger the denial of service?
An attacker needs to supply a crafted multi-frame MPO or DCX image with a small initial frame and a later frame that declares dimensions beyond Pillow's decompression-bomb limit. Exploitation requires the application to process that later frame; no attacker privileges are required, but user interaction is indicated by the supplied severity vector.
What should be done if upgrading is not immediately possible?
Avoid processing untrusted MPO and DCX files beyond their first frame. In particular, do not iterate frames or call seek() on untrusted files until Pillow can be updated to version 13.0.0.
How can I determine whether my application is affected?
Check whether it uses Pillow versions earlier than 13.0.0 and accepts or processes MPO or DCX images from untrusted sources. Applications that iterate image frames, use ImageSequence.Iterator, or call seek() and load() on these formats are affected by the described bypass.