KEVWire

Security intelligence, with context.

Response snapshot Oct 26, 2026, 23:51 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-112401

Pillow: Quadratic GIF comment concatenation allows DoS in Image.open()

``` ============================================================================ Quadratic GIF comment concatenation allows DoS in Image.open() Location: src/PIL/GifImagePlugin.py:266 in GifImageFile.

At a glance

Exploited
Not known
Severity
6.9 Medium (CVSS 4.0)
Chance of exploitation
0.5% in the next 30 days
Sources
1 source

What to do

GitHub lists first patched versions: Pillow: 13.0.0. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-74r2-7ff9-mjj2 (opens a new tab)

What happened

``` ============================================================================ Quadratic GIF comment concatenation allows DoS in Image.open() Location: src/PIL/GifImagePlugin.py:266 in GifImageFile._seek() Commit: a707c6b21c313880d0771e54965d142dd42358b6 ============================================================================ ```

Opening a crafted GIF file with `Image.open()` takes time that grows with the square of the file's size, so anyone who can submit an image to a service that uses Pillow can keep one worker busy at full processor load for a long time with a small file. A 2 MB file takes about 5 seconds to open, a 4 MB file about 35 seconds, and an 8 MB file about 3 minutes.

An ordinary GIF of the same size opens in well under a second. The cost is paid inside `Image.open()` itself, with default settings, before the application has seen the image's size or format.

The bug is in `GifImageFile._seek()` in `src/PIL/GifImagePlugin.py`, which builds `info["comment"]` by appending to an immutable `bytes` object with no size limit. It appends once per 255-byte sub-block (`comment += block`, line 261) and once per comment extension (`info["comment"] += b"\n" + comment`, line 266). Each append copies everything collected so far.

Continue reading

A file made of many tiny comment extensions forces a full re-copy for each one. Because `_open()` ends with `self._seek(0)`, any code that only opens an uploaded file to identify it, read its dimensions, or make a thumbnail pays the full cost. The decompression-bomb check does not help, since the image is 1×1.

Memory use stays linear, so the effect is processor exhaustion and latency, bounded by whatever upload size the service allows (about 1 second at 1 MB). This is the same kind of issue as the EPS open-phase slowdown fixed as CVE-2021-28677, here in the GIF comment handling.

The quadratic pattern was introduced in commit b2b737b68b06 ("Allow arbitrary number of comment extension subblocks", November 2018) and has shipped since Pillow 5.4.0. The attached script builds 2 MB and 4 MB test files and compares each against a byte-identical control whose extensions are labelled as plain text instead of comment. The 4 MB comment file took about 34 seconds to open, against 0.

26 seconds for its control. A patch that collects the pieces in a list and joins them once is also attached.

## Details

`GifImageFile._seek()` collects GIF comment extensions into `info["comment"]` using `bytes` concatenation, once per 255-byte sub-block and once per comment extension, with no limit on the total. `bytes` is immutable, so every `+=` allocates a new object and copies everything accumulated so far; the work done by `Image.open()` is therefore quadratic in the amount of comment data in frame 0.

A 1×1 GIF89a whose body is nothing but the 5-byte comment extension `21 FE 01 41 00` repeated makes this directly observable. `Image.open(BytesIO(data))` on such a file, stock build at this commit, 2-CPU machine:

| file size | comment extensions (`21 FE …`) | same bytes labelled plain-text extension (`21 01 …`) | |---|---|---| | 1 MB | 1.2 s | 0.09 s | | 2 MB | 4.9 s | 0.13 s | | 4 MB | 35 s | 0.26 s | | 8 MB | 183 s | — |

The control column is the identical byte stream with the extension label changed from `0xFE` to `0x01`; it goes through the same loop and the same `self.data()` reads but is discarded instead of concatenated, and scales linearly. No `load()` is needed and no warning or exception is raised; `im.info["comment"]` simply ends up holding the multi-megabyte blob.

The code, `src/PIL/GifImagePlugin.py:229-281` (trimmed):

```python elif s == b"!": s = self.fp.read(1) block = self.data() … elif s[0] == 254: # # comment extension # comment = b""

# Read this comment block while block: comment += block # :261 one copy per sub-block block = self.data()

if "comment" in info: # If multiple comment blocks in frame, separate with \n info["comment"] += b"\n" + comment # :266 one copy per extension else: info["comment"] = comment s = b"" continue … while self.data(): # every other extension: read and drop pass ```

`self.data()` (`:97-102`) reads one length byte and then that many bytes, so each call is cheap and bounded; the cost is entirely in the two `+=` lines. For a single comment extension of N bytes delivered as N/255 sub-blocks, line 261 copies about N²/510 bytes in total (4 MB → 2.3 s, 8 MB → 9.5 s, 16 MB → 38 s measured).

Splitting the same data into many small comment extensions is worse, because line 266 re-copies the whole of `info["comment"]` once per extension: with 1-byte extensions a file of F bytes causes roughly F²/25 bytes of copying, which is the table above (growth ×4–7 per doubling once the buffer outgrows the CPU caches).

This runs during `Image.open()`: `_open()` ends with `self._seek(0)` (`:135`), so any application that merely opens an uploaded file to identify it, read its size, or thumbnail it pays the full cost before it has made any decision about the image. `Image._decompression_bomb_check` is not involved (the image is 1×1), and unlike PNG text chunks (`PngImagePlugin.

MAX_TEXT_CHUNK` / `MAX_TEXT_MEMORY`) there is no cap on GIF comment size. Later frames repeat the same loop for their own comments via `seek()` / `n_frames`.

With both `+=` sites replaced by list-append and a single `b"".join()` (as in the attached patch), the same 16 MB single-extension file opens in 0.03 s, confirming the concatenation is the whole cost.

## Impact

Anyone who can get a GIF to a service that calls `PIL.Image.open()` on it — avatar/media uploads, thumbnailers, format sniffing, anything using `ImageOps`/`thumbnail()` on user files — can pin one worker process at 100 % CPU for about 5 s with a 2 MB file, 35 s with a 4 MB file and 3 minutes with an 8 MB file, under default settings, with the time growing quadratically in the permitted upload size.

A benign GIF of the same size opens in well under a second, so request-cost assumptions based on fi

Affected package ranges reported by GitHub: Pillow: >= 5.4.0, < 13.0.0

Scores and how they were measured

CVSS base severity

6.9

Version 4.0As of

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

0.5%

Scored Percentile 41.1%

Model and measurement source (opens a new tab)

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-74r2-7ff9-mjj2 (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  3. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  4. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  5. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  6. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  7. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  8. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  9. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  10. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  11. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-74r2-7ff9-mjj2, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  12. Primary source

    FIRST measured EPSS score (opens a new tab)

    Measured EPSS probability and percentile for CVE-2026-112401, scored 2026-10-26. This is a model estimate, not proof of exposure.

    Published Checked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 21). Pillow: Quadratic GIF comment concatenation allows DoS in Image.open() (CVE-2026-112401). https://kevwire.com/record/CVE-2026-112401

MLA

"Pillow: Quadratic GIF comment concatenation allows DoS in Image.open() (CVE-2026-112401)." KEVWire, 21 Oct. 2026, kevwire.com/record/CVE-2026-112401.

Chicago

KEVWire. "Pillow: Quadratic GIF comment concatenation allows DoS in Image.open() (CVE-2026-112401)." Last modified October 21, 2026. https://kevwire.com/record/CVE-2026-112401.

Evidence summary (plain text)

Pillow: Quadratic GIF comment concatenation allows DoS in Image.open() (Oct 21, 2026).
```
============================================================================
Quadratic GIF comment concatenation allows DoS in Image.open()
Location: src/PIL/GifImagePlugin.py:266 in GifImageFile._seek()
Commit: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
```

Opening a crafted GIF file with `Image.open()` takes time that grows with the square of the file's size, so anyone who can submit an image to a service that uses Pillow can keep one worker busy at full processor load for a long time with a small file. A 2 MB file takes about 5 seconds to open, a 4 MB file about 35 seconds, and an 8 MB file about 3 minutes. An ordinary GIF of the same size opens in well under a second. The cost is paid inside `Image.open()` itself, with default settings, before the application has seen the image's size or format.
CVSS 6.9 (version 4.0; evidence as of Oct 21, 2026; https://github.com/advisories/GHSA-74r2-7ff9-mjj2).
EPSS 0.5% for the following 30 days, scored Oct 26, 2026 (https://api.first.org/data/v1/epss?cve=CVE-2026-112401&date=2026-10-26).
Sources:
GitHub-reviewed advisory GHSA-74r2-7ff9-mjj2: https://github.com/advisories/GHSA-74r2-7ff9-mjj2
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMlQxNTo1Mjo1NlrOAAfxiA%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMlQxNDowNjoxNFrOAAfxZw%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMlQwODoyNzoyM1rOAAYp9A%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMVQyMTozNTo1OFrOAAfwCQ%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMVQyMTozMDoxM1rOAAfv2A%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMVQyMToyODowOVrOAAfv1g%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMVQyMToyNDoyMFrOAAfv1Q%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yMVQyMDo0NTowN1rOAAfvzQ%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QxODozMDoyN1rOAAY_5Q%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QxODoyOTowNVrOAAfz8g%3D%3D
FIRST measured EPSS score: https://api.first.org/data/v1/epss?cve=CVE-2026-112401&date=2026-10-26
Record ID: CVE-2026-112401.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 26, 2026, 19:40 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are not available yet. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail