Skip to main content
VulnerabilityPublished Updated

CVE-2026-112399: Pillow · APNG background disposal allocates full canvas before decompression bomb check

Affects aiderSource GCVE Database

0
Compare
Authoritative record:CVE-2026-112399· High 8.7

Not known to be exploited · CVSS 8.7 High.

Data as of 30 Oct 2026, 07:30 UTC

CVSS 4.0 base score
8.7High
How severe the flaw is on the 0–10 CVSS scale.

Thin evidence

One scorer or none
CVSS scores by source
CNA (GitHub_M)CVSS 4.08.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 19 Oct 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponized: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 29 Oct 2026.

What to do

Affected software

ProductAffectedFixed in
Homebrewaiderpkg:brew/aiderOther threatsAffected
  • >=0.86.1_1
Fixed in–

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

``` ============================================================================ APNG background disposal allocates full canvas before decompression bomb check Location: src/PIL/PngImagePlugin.py:1000 in PngImageFile._seek() Commit: a707c6b21c313880d0771e54965d142dd42358b6 ============================================================================ ``` A 126-byte animated PNG file makes Pillow's `Image.open()` allocate and fill memory sized by the image dimensions the file claims, before Pillow's built-in check for oversized images runs. A file claiming 22000×22000 pixels costs about 1.9 GB. Anyone who can get an application to open an image they supply, with default settings and no further calls such as `load()`, can cause a multi-gigabyte memory spike per file. If the claimed size exceeds the machine's free memory, the operating system kills the process outright. The bug is in `PngImageFile._seek()` in `src/PIL/PngImagePlugin.py` (around line 1000). For any APNG, `_open()` calls `self._seek(0)`. When the first `fcTL` chunk requests `OP_BACKGROUND` disposal, `_seek()` calls `Image.core.fill(self.mode, self.size)` with the full IHDR size, which allocates and zero-fills width×height×4 bytes. `OP_PREVIOUS` takes the same path, because frame 0 rewrites it to `OP_BACKGROUND`. This all happens inside the plugin factory, so `Image.open()`'s `_decompression_bomb_check(im.size)` only runs after the memory has been touched. A 1×1 frame rectangle passes the only checks on the way. Below available memory, the allocation is freed when `DecompressionBombError` propagates, so sustained pressure needs repeated or concurrent uploads. A 45000×45000 header gets the process killed by the out-of-memory killer on an 8 GB host. The attached reproducer opens two such files that differ in one byte (`dispose_op` 0 vs 1). Both end in `DecompressionBombError`, but peak resident memory is 17 MB for the first and 1864 MB for the second. The eager fill was introduced in 5e4e0fa6ee67 ("Use disposal settings from previous frame", December 2020) and first shipped in 8.1.0. Two candidate patches with a regression test are attached. ## Details For an APNG whose first `fcTL` chunk requests `OP_BACKGROUND` disposal, `PngImageFile._open()` creates and zero-fills a core image of the full IHDR-declared size. This happens inside the plugin factory, i.e. before `Image.open()` gets to `_decompression_bomb_check(im.size)`, so the check that is meant to reject oversized headers without allocating anything runs only after the allocation has been made and touched. The following 126-byte file (all chunk lengths and CRCs valid) makes `Image.open(io.BytesIO(data))`, with default `MAX_IMAGE_PIXELS` and no `load()`, reach 1.86 GB resident (`ru_maxrss`) and then raise `DecompressionBombError`. The byte-identical file with `dispose_op=0` raises the same error at 17 MB. ``` IHDR width=22000 height=22000 bit_depth=8 colour_type=6 (RGBA) acTL num_frames=2 num_plays=0 fcTL seq=0 w=1 h=1 x=0 y=0 delay=1/10 dispose_op=1 (OP_BACKGROUND) blend_op=0 IDAT zlib(00 00 00 00 00) # one 1x1 RGBA scanline IEND ``` With `width=height=45000` (8.1 GB canvas) the same call attempts the full allocation: under a 4 GiB `RLIMIT_AS` it fails with `MemoryError` from the allocator instead of `DecompressionBombError`; without a cap, on an 8 GB host with no swap, the process is OOM-killed. ### Code path `Image.open()` runs the bomb check only after the plugin's constructor (and therefore `_open()`) has returned: ```python # src/PIL/Image.py:3699-3701 im = factory(fp, filename) _decompression_bomb_check(im.size) return im ``` For a plain PNG that ordering is fine: `PngImageFile._open()` only parses chunks up to the first `IDAT` and records `self._size` from `IHDR`; nothing is allocated from it. For an APNG (`acTL` seen, so `im_n_frames` is set) `_open()` additionally positions on frame 0: ```python # src/PIL/PngImagePlugin.py:845-853 if self.png.im_n_frames is not None: … self._seek(0) ``` `_seek(0)` copies `disposal` and `bbox` from the `fcTL` chunk into `self.dispose_op` / `self.dispose_extent` and then prepares the disposal image for the *next* frame eagerly, using the full image size: ```python # src/PIL/PngImagePlugin.py:991-1001 if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS: self.dispose_op = Disposal.OP_BACKGROUND self.dispose = None if self.dispose_op == Disposal.OP_PREVIOUS: … elif self.dispose_op == Disposal.OP_BACKGROUND: self.dispose = Image.core.fill(self.mode, self.size) self.dispose = self._crop(self.dispose, self.dispose_extent) ``` `Image.core.fill` is `_fill()` in `src/_imaging.c:711`, which does `ImagingNewDirty(mode, xsize, ysize)` followed by `ImagingFill()`; `ImagingFill` (`src/libImaging/Fill.c:23`) writes every row, so the whole `

References

Community reviews

Loading reviews…