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:
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
References
Summary
Pillow's decompression-bomb protection (
Image._decompression_bomb_check, gated byImage.MAX_IMAGE_PIXELS) is enforced only on the first frame, insideImage.open(). For multi-frame formats,seek()re-parses the target frame's header and resetsself._size.GifImagePluginandTiffImagePluginre-run the bomb check onseek(), butMpoImageFile.seek()andDcxImageFile.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 vulnerableseek()methods re-parse a new frame and set a newself._sizewithout 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). CompareGifImagePlugin._seekandTiffImagePlugin, which call_decompression_bomb_checkcorrectly on seek.PoC
Self-contained; the victim uses default
MAX_IMAGE_PIXELS:Measured on Pillow 12.3.0 (default limit 89,478,485 px; error threshold 178,956,970 px):
DecompressionBombError(correctly blocked)The DCX vector (
0x3ADE68B1magic + offset directory + two PCX pages) behaves identically viaDcxImageFile.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.jpgextension 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:References