high · 8.7 Patched

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

CVE-2026-112400 · GHSA-65qm-jvmh-6pqg · published 20 Oct 2026

How serious is it

  • Severity: high, CVSS score 8.7 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-112400?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • Pillow (PyPI) >= 9.2.0, < 13.0.0

How to check

  1. Run pip show Pillow in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade Pillow to 13.0.0 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

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

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database, OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON