Skip to main content
VulnerabilityPublished Updated

CVE-2026-112894: Pillow · Quadratic-time DoS in PDF save_all (PdfParser.next_object_id)

Affects aiderSource GCVE Database

0
Compare
Authoritative record:CVE-2026-112894· 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.
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

``` ============================================================================ Quadratic-time DoS in PDF save_all (PdfParser.next_object_id) Location: src/PIL/PdfParser.py:629 in next_object_id() Commit: a707c6b21c313880d0771e54965d142dd42358b6 ============================================================================ ``` Pillow's PDF writer takes time proportional to the square of the number of frames when it saves a multi-frame image as a multi-page PDF. Anyone who can get an application to convert their uploaded image this way can tie up a worker at full CPU for minutes with a small file. A 240 KB animated image takes about 70 seconds, a 720 KB one an estimated 10 minutes, and a 125-byte animated PNG that merely *declares* a large frame count triggers the same slowdown with no upper bound short of 2³¹ frames. Memory stays under 100 MB and the decompression-bomb limit never applies. The cause is `PdfParser.next_object_id()` in `src/PIL/PdfParser.py`, which finds the next free id by computing `max(self.xref_table.keys())`. `XrefTable.keys()` rebuilds three sets over every id reserved so far on each call. `PdfImagePlugin._save()` calls it three times (four for palette images with transparency) per frame, for `n_frames` frames, before any page is written. That makes the id-reservation loop alone O(N²). Because `n_frames` is taken from the file before any frame is seeked, APNG's `acTL` count drives the loop even when the frames don't exist. In that case the save raises `OSError` only after the full delay. The precondition is that the application calls `im.save(..., "PDF", save_all=True)` on the attacker's file; `Image.open()` and `load()` alone do not reach this code. The attached reproducer shows 4× the frames costing about 11× the CPU for a valid GIF and about 16× for a fixed 125-byte APNG. The behaviour was introduced in `6207b44ab1ff` (2018-01-18) and first shipped in Pillow 5.1.0. A small patch that tracks the highest id in `XrefTable` makes the save linear. ## Details Saving a multi-frame image as a multi-page PDF (`im.save(fp, "PDF", save_all=True)`) costs CPU time proportional to the square of the frame count, independent of pixel content, because every object id the PDF writer reserves is found by rebuilding and scanning the set of all ids reserved so far. A 1×1-pixel animated GIF made of N identical 15-byte frames (built by the reproducer) gives, on a 2-CPU container with the stock build at this commit (absolute timings vary by machine; the growth rate does not): | frames | GIF size | `im.save(BytesIO(), "PDF", save_all=True)` | peak RSS | |---|---|---|---| | 2 000 | 30 KB | 1.1 s | | | 8 000 | 120 KB | 14.7 s | | | 16 000 | 240 KB | 70 s | 60 MB | | 32 000 | 480 KB | 262 s | 95 MB | | 48 000 | 720 KB | still running at 120 s (extrapolated ≈ 10 min) | | Each doubling of the input roughly quadruples the time. `cProfile` of the 8 000-frame save attributes 13.5 s of 17.6 s to 24 003 calls of `PdfParser.next_object_id` (8.9 s inside `XrefTable.keys`, 3.9 s inside `max`); encoding and writing the 8 000 pages is the remaining ~4 s and scales linearly. The per-page id reservation loop runs before any page is written: ```python # src/PIL/PdfImagePlugin.py:247-259 (_save) for im in ims: im_number_of_pages = 1 if save_all: im_number_of_pages = getattr(im, "n_frames", 1) … for i in range(im_number_of_pages): image_refs.append(existing_pdf.next_object_id(0)) if im.mode == "P" and "transparency" in im.info: image_refs.append(existing_pdf.next_object_id(0)) page_refs.append(existing_pdf.next_object_id(0)) contents_refs.append(existing_pdf.next_object_id(0)) ``` `n_frames` comes straight from the file (for GIF it is the number of image blocks; the GIF plugin counts them by walking block headers, which is cheap). The frames do not even have to exist: the loop runs on `n_frames` before any frame is seeked, and for APNG `n_frames` is the `num_frames` value declared in the `acTL` chunk, rejected only if it is 0 or above `0x80000000` (`src/PIL/PngImagePlugin.py:716-720`). A 125-byte APNG declaring 8 000 frames therefore costs the same ~10–13 s as the 120 KB GIF, and more without bound as the declared count grows. Each of the 3 (or 4, for `P` images with transparency) calls per frame lands here: ```python # src/PIL/PdfParser.py:626-634 def next_object_id(self, offset: int | None = None) -> IndirectReference: try: # TODO: support reuse of deleted objects reference = IndirectReference(max(self.xref_table.keys()) + 1, 0) except ValueError: reference = IndirectReference(1, 0) if offset is not None: self.xref_table[reference.object_id] = (offset, 0) return reference # src/PIL/PdfParser.py:175-178 (XrefTable) def keys(self) -> set[int]: return ( set(self.existing_entries.keys()) - set(se

References

Community reviews

Loading reviews…