---
id: "CVE-2026-112400"
kind: "vulnerability"
aliases: ["CVE-2026-112400", "GHSA-65qm-jvmh-6pqg"]
title: "Pillow: GIF seek keeps rejected size after DecompressionBombError, allowing DoS"
severity: "high"
cvss: 8.7
epss: 0.0029100000392645597
exploited: false
status: "open"
category: "package"
products: [{"name": "Pillow", "fixed": ["13.0.0"], "affected": ">= 9.2.0, < 13.0.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-400", "image-upload", "package", "pillow", "python", "self-hosted"]
not_affected_if: ["default-config", "no-frame-seek-with-caught-errors", "pillow-below-9.2.0-or-13.0.0-plus"]
published_at: "2026-10-19T22:17:07+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:19+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-65qm-jvmh-6pqg", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112400", "first_seen_at": "2026-10-23T21:57:19+00:00"}]
---

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

Pillow's GIF frame seek stores a rejected oversized frame size on the image object before the decompression-bomb check raises, so a caught error leaves im.size wrong. A 60-byte GIF can then make later operations allocate gigabytes of memory, causing denial of service in applications that catch the error and keep using the same image.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:Pillow affected >= 9.2.0, < 13.0.0 fixed 13.0.0

## How to check

pip show pillow (affected if version >= 9.2.0 and < 13.0.0)
grep -rn "DecompressionBombError" your application code and check whether it catches the error around im.seek() and keeps using the same image

## Fix

Upgrade Pillow to 13.0.0 or later (pip install -U "pillow>=13.0.0")
Until then, discard the image object after catching DecompressionBombError instead of reusing it.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-65qm-jvmh-6pqg
- 2026-10-23T21:57:19+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-112400

## From the sources

```
============================================================================
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:

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

- Retrying `im.seek(1)`. The failed `_seek` left `self.__offset == 0` and the
  file pointer just past the 9-byte descriptor, so the retry parses whatever
  follows — in the file above, a valid 1×1 frame — and succeeds. `im.load()`
  then runs `GifImageFile.load_prepare()`:
  ```python
  # src/PIL/GifImagePlugin.py:452-456
  elif self.mode in ("RGB", "RGBA"):
      self._prev_im = self.im
      if self._frame_palette:
          self.im = Image.core.fill("P", self.size, self._frame_transparency or 0)
  ```
  which allocates and `memset`s a 40000×40000 "P" image (1.6 GB) to decode a
  1×1 tile into, and `load_end()` (`GifImagePlugin.py:485-491`) fills a second
  one and converts it to RGB (another 1.6 GB + 6.4 GB).
- `im.rotate(angle, expand=True)` for a non-multiple-of-90 angle, or the
  common flatten-onto-background idiom `Image.new("RGB", im.size, bg)`, allocate
  from `im.size` as well (measured on an object poisoned to 20000×20000:
  `rotate(45, expand=True)` completes and touches 776 MiB;
  `Image.new("RGB", im.size, "white")` touches 1.5 GiB). Operations that pass
  `im.size` as a box against the real buffer (`thumbnail`, `resize`, `save`)
  fail with `ValueError`/`SystemError` instead.

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

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
