KEVWire

Security intelligence, with context.

Response snapshot Oct 26, 2026, 22:47 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-112901

Pillow: ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes

``` ============================================================================ ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes Location: src/PIL/XbmImagePlugin.py:58 in XbmImageFile.

At a glance

Exploited
Not known
Severity
6.9 Medium (CVSS 4.0)
Chance of exploitation
0.4% 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-xgh8-jp6q-p5x8 (opens a new tab)

What happened

``` ============================================================================ ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes Location: src/PIL/XbmImagePlugin.py:58 in XbmImageFile._open() Commit: a707c6b21c313880d0771e54965d142dd42358b6 ============================================================================ ```

A crafted 512-byte file makes Pillow's `Image.open()` spend about six minutes of processor time on one core before it gives up with `UnidentifiedImageError`. Anyone who can hand bytes to an application that opens images with Pillow's default settings can trigger this, for example through an upload form, a thumbnailer, or a service that checks whether a blob is an image. A few such files sent at once can tie up a typical pool of worker processes.

The cause is the `xbm_head` regular expression in `src/PIL/XbmImagePlugin.py:32-40`, which `XbmImageFile._open()` (line 58) runs over the first 512 bytes of any stream whose prefix starts with `#define`. Several adjacent parts of the pattern can match the same characters. `[ \t]+` overlaps with `.

*` in the width and height lines and with `[^_]*` in the optional hotspot lines, and `[\r\n]+` closing the hotspot group overlaps with the following `[\000-\377]*`. The header sets a run of k spaces after each `#define` and a run of k line terminators after `_y_hot`, then omits the `[]` after `_bits`.

Continue reading

That forces Python's backtracking matcher to try every way of splitting those runs and rescan the remaining bytes each time. The cost grows roughly as k to the fifth power: k = 60 measured 375 s, while a well-formed XBM of the same size opens in about 40 ms.

Because this happens before any format is identified, none of Pillow's size, pixel-count or decompression-bomb limits apply, and upload size limits do not help because the file is tiny. `ImageFile.Parser.feed()` repeats the cost on every chunk until the stream is identified. The only existing mitigation is passing a `formats=` allowlist that excludes `"XBM"`.

The attached reproducer opens the same header with and without the trailing `[]`. The valid version opens as XBM in 52 ms. The broken version takes 0.1 s, 0.75 s, 3.0 s and 8.6 s at k = 10, 15, 20 and 25, and the slow k = 60 case is not run. This is the same class of bug as CVE-2021-25292 in `PdfParser`, but in a different plugin and reachable from plain `Image.open()`.

## Details

`XbmImageFile._open()` identifies an XBM file by running the module-level regex `xbm_head` over the first 512 bytes of the stream. The pattern has several adjacent quantifier pairs that can consume the same characters, followed by a `[\000-\377]*_bits\[]` tail that rescans the rest of the buffer for every way of splitting them.

A 512-byte input built to exercise all of them and then fail the final literal keeps `Image.open()` in `xbm_head.match()` for minutes before it raises `UnidentifiedImageError`.

This 512-byte input (k = 60) takes 375 s of CPU in `Image.open(io.BytesIO(data))` with default settings, measured on the stock build at this commit:

```python k = 60 data = b"#define" + b" "*k + b"_width 1\r" data += b"#define" + b" "*k + b"_height 1\r" data += b"#define" + b" "*k + b"_x_hot 1\r" data += b"#define" + b" "*k + b"_y_hot 1" + b"\r"*k data += b"_bits" # no "[]": the final literal never matches data += b"A" * (512 - len(data)) # pad to the 512 bytes _open() reads ```

Calling `XbmImagePlugin.xbm_head.match(data)` directly gives the same picture: k = 10 → 0.11 s, k = 15 → 0.76 s, k = 20 → 3.0 s, k = 25 → 8.5 s, k = 30 → 19.9 s (roughly ×27 per doubling of k, i.e. about k^4.7). A well-formed 512-byte XBM matches in 10 µs and opens and loads through `Image.open()` in ~40 ms including plugin import; the same k = 60 header with `_bits[]` present matches in 5 µs.

The regex, `src/PIL/XbmImagePlugin.py:32`:

```python xbm_head = re.compile( rb"\s*#define[ \t]+.*_width[ \t]+(?P [0-9]+)[\r\n]+" b"#define[ \t]+.*_height[ \t]+(?P [0-9]+)[\r\n]+" b"(?P " b"#define[ \t]+[^_]*_x_hot[ \t]+(?P [0-9]+)[\r\n]+" b"#define[ \t]+[^_]*_y_hot[ \t]+(?P [0-9]+)[\r\n]+" b")?" rb"[\000-\377]*_bits\[]" ) ```

and the only call site, `src/PIL/XbmImagePlugin.py:58`:

```python def _open(self) -> None: … m = xbm_head.match(self.fp.read(512)) if not m: msg = "not a XBM file" raise SyntaxError(msg) ```

For a well-formed header (`#define im_width 8\n…static char im_bits[] = {`) each `[ \t]+` eats the single space, `.*`/`[^_]*` eat the identifier prefix, and `[\000-\377]*_bits\[]` finds the literal on its first backward scan, so the match is linear. The crafted header diverges at every one of those points:

- `[ \t]+.*_width` and `[ \t]+.*_height`: the run of k spaces after `#define` can be split between `[ \t]+` and `.*` in k ways, all leading to the same continuation. Factor k each. (The choice of `\r` terminators is incidental: the same header with `\n` terminators is equally slow.)

- `[ \t]+[^_]*_x_hot` and `[ \t]+[^_]*_y_hot` inside the optional `hotspot` group: `[^_]*` also matches spaces, so the same k-way split exists twice more. Factor k each. These two lines are essential: without them the same input is rejected in about 10 ms. - `[\r\n]+` closing the `hotspot` group, followed by `)?

` and `[\000-\377]*`: the run of k `\r` bytes after `_y_hot 1` can be divided between `[\r\n]+` and `[\000-\377]*` in k ways. Factor k. - `[\000-\377]*_bits\[]`: for every combination above, the engine swallows the remaining ~150 bytes of padding and walks back looking for `_bits[]`. The input contains `_bits` but never `[]`, so this always fails and every combination is visited.

CPython's `re` is a backtracking engine without memoisation, so the total work is on the order of k^5 × (512 − header length) steps — about 10^11 for k = 60, consistent with the measured 375 s. The 512-byte read caps k (5k + ~60 ≤ 512), so the cost per `open()` is bounded, but the bound is minutes of CPU.

The path is the default one. `Image.open()` (`src/PIL/Image.py:3719`

Affected package ranges reported by GitHub: Pillow: <= 12.3.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.4%

Scored Percentile 35.8%

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-xgh8-jp6q-p5x8 (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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-xgh8-jp6q-p5x8, 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-112901, 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: ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes (CVE-2026-112901). https://kevwire.com/record/CVE-2026-112901

MLA

"Pillow: ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes (CVE-2026-112901)." KEVWire, 21 Oct. 2026, kevwire.com/record/CVE-2026-112901.

Chicago

KEVWire. "Pillow: ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes (CVE-2026-112901)." Last modified October 21, 2026. https://kevwire.com/record/CVE-2026-112901.

Evidence summary (plain text)

Pillow: ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes (Oct 21, 2026).
```
============================================================================
ReDoS in XbmImagePlugin: 512-byte file stalls Image.open() for minutes
Location: src/PIL/XbmImagePlugin.py:58 in XbmImageFile._open()
Commit: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
```

A crafted 512-byte file makes Pillow's `Image.open()` spend about six minutes of processor time on one core before it gives up with `UnidentifiedImageError`. Anyone who can hand bytes to an application that opens images with Pillow's default settings can trigger this, for example through an upload form, a thumbnailer, or a service that checks whether a blob is an image. A few such files sent at once can tie up a typical pool of worker processes.
CVSS 6.9 (version 4.0; evidence as of Oct 21, 2026; https://github.com/advisories/GHSA-xgh8-jp6q-p5x8).
EPSS 0.4% for the following 30 days, scored Oct 26, 2026 (https://api.first.org/data/v1/epss?cve=CVE-2026-112901&date=2026-10-26).
Sources:
GitHub-reviewed advisory GHSA-xgh8-jp6q-p5x8: https://github.com/advisories/GHSA-xgh8-jp6q-p5x8
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=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QxODozNTowMVrOAAZRcw%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-112901&date=2026-10-26
Record ID: CVE-2026-112901.
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