high

CVE-2026-112400

PyPI · Pillow

Summary

Pillow: GIF seek keeps rejected size after DecompressionBombError, allowing DoS

Severity
high
EPSS
0.5% (p43)
CWE
CWE-400
Also known as
GHSA-65qm-jvmh-6pqg
Published
2026-10-20
Updated
2026-10-20

Advisory details

============================================================================
GIF seek keeps rejected size after DecompressionBombError, allowing DoS
Location: src/PIL/GifImagePlugin.py:293  in GifImageFile._seek()
Commit:   a707c6b21c313880d0771e54965d142dd42358b6
============================================================================

A GIF of about 60 bytes can make Pillow allocate gigabytes of memory. This affects applications that catch the "image too large" error raised while stepping to a later animation frame and then keep using the same image object. Pillow correctly refuses the oversized frame, but it stores the refused dimensions on the image object anyway, and later operations size their memory from them. An attacker who can upload a GIF to such a service can exhaust its memory: the process either fails with an out-of-memory error after committing gigabytes, or is killed by the operating system. There is no memory corruption.

The bug is in GifImageFile._seek() (src/PIL/GifImagePlugin.py:292-294). When a frame's extent enlarges the image, the code assigns the new size to self._size and only then calls Image._decompression_bomb_check(). Nothing restores the old size when DecompressionBombError is raised, because seek() only rolls back on EOFError. After the caught error, im.size reports the rejected size while the pixel buffer im.im is still the 1×1 first frame. Because the extent is a 16-bit offset plus a 16-bit size, that rejected size can be as large as 131070×131070.

In the attached reproducer, the rejected size is 40000×40000. A retried seek(1) followed by load() then reaches load_prepare(), which calls Image.core.fill() with that size to decode a single pixel. Peak memory rises from 17 MiB to 1700 MiB before a 3 GiB address-space cap stops the next allocation. Image.new("RGB", im.size, ...) and rotate(..., expand=True) also allocate from the stale size.

The default paths are unaffected: plain Image.open() plus load(), ImageSequence.Iterator, and n_frames/is_animated. The ordering was introduced in c9f1b35e9810 ("Added GIF decompression bomb check", 2022-06-30, first released in 9.2.0), the check added for CVE-2022-45198. The two dispose-extent checks later in the same function already validate before using the size, and the attached patch makes this check follow the same order.

Details

GifImageFile._seek() lets a later frame enlarge the image, and performs the decompression-bomb check for the new size only after storing it on the object:

# src/PIL/GifImagePlugin.py:289-294  (GifImageFile._seek)
x0, y0 = i16(s, 0), i16(s, 2)
x1, y1 = x0 + i16(s, 4), y0 + i16(s, 6)
if (x1 > self.size[0] or y1 > self.size[1]) and update_image:
    self._size = max(x1, self.size[0]), max(y1, self.size[1])   # state mutated
    Image._decompression_bomb_check(self._size)                 # ...then validated

_decompression_bomb_check() (src/PIL/Image.py:3586) raises DecompressionBombError for anything above 2 * MAX_IMAGE_PIXELS (178 956 970 px by default), but by then self._size already holds the rejected value and nothing restores it: seek() (GifImagePlugin.py:166-179) only rolls back on EOFError, and _seek(0) only ever grows _size. The object that the application still holds therefore reports im.size == (40000, 40000) while im.im is the 1×1 buffer of frame 0. Compare the two dispose-extent checks a few lines further down in the same function (GifImagePlugin.py:383 and :407), which call _decompression_bomb_check(dispose_size) before the corresponding Image.core.fill(), and TiffImagePlugin.load_prepare() (src/PIL/TiffImagePlugin.py:1319-1321), which checks _tile_size before allocating from it.

With this 60-byte GIF (hex)

47494638396101000100800000000000ffffff          GIF89a, 1x1 screen, 2-entry global palette
2c000000000100010000 02 024401 00               frame 0: 1x1 @ (0,0), LZW data
2c00000000409c409c00                            frame 1 descriptor: 40000x40000 @ (0,0)
2c000000000100010000 02 024401 00               another 1x1 frame
3b                                              trailer

Image.open() succeeds (im.size == (1, 1)), im.seek(1) raises DecompressionBombError: Image size (1600000000 pixels) exceeds limit of 178956970 pixels, and afterwards im.size == (40000, 40000), im.im.size == (1, 1), im.tell() == 0.

What happens next depends on what the application does with the object, but several ordinary operations take their dimensions from im.size rather than from the C buffer:

The extent fields are 16-bit offset plus 16-bit size, so the stored size can be pushed to 131070×131070 (17.2 Gpx; 17 GB for mode "P", 69 GB for "RGB"/"RGBA" storage), 96× the error threshold.

n_frames / is_animated are not affected: they call _seek(frame, update_image=False), which skips the _size assignment (the file above reports n_frames == 2, is_animated == True and leaves im.size at 1×1).

Impact

A remote user who can upload a GIF to a service that (a) seeks animation frames and (b) catches the exception from seek() — a broad except Exception around a while True: im.seek(im.tell()+1) frame-collection loop, or an explicit except DecompressionBombError: log and continue — and then keeps using the same Image object, ends up driving allocations of 1.6 GB to tens of GB from a 60-byte file: MemoryError after the process has committed gigabytes, or an OOM kill of the worker when no address-space limit is set. The MAX_IMAGE_PIXELS guard fires, but does not actually keep the rejected dimensions out of the object it is guarding.

The precondition is real application behaviour but not the default path: Image.open(); im.load() is unaffected, and ImageSequence.Iterator propagates the error out of the for statement (a try inside the loop body does not swallow it). Hence low severity. This is adjacent to the GIF extent bomb check added for CVE-2022-45198: the ordering was introduced with that check in c9f1b35e9810 ("Added GIF decompression bomb check", 2022-06-30), first released in Pillow 9.2.0. Before it, the same size growth happened with no check at all; the check introduced there is the one that runs too late.

Reproducing

repro is a single Python script run against the installed Pillow. It needs about 1.7 GB of free RAM.

  1. setup_env() caps the process address space at 3 GiB with RLIMIT_AS, so the oversized allocations end in MemoryError instead of the OOM killer. It then opens the 60-byte GIF shown above, built in memory by bomb_gif(). The GIF contains a 1×1 screen and a 1×1 frame 0, so Image.open() passes its own bomb check. Next comes a bare frame-1 image descr

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.