All vulnerabilities
CVE-2026-96643GHSA-g4qv-2jxx-769h

Pillow: PNG eXIf chunk makes EXIF parsing run in quadratic time

python-pillow PillowpipLibraries & packages
CVSS
7.1High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)0.33%Chance of exploitation activity in the next 30 days, higher than 25% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed in13.0.0Fixed versionGitHub
Published21 Oct 2026Updated 23 Oct 2026CVE List

What fixes it

  • 13.0.0Pillow, pipGitHub
  • 13.0.0pillow, PyPIOSVpip install "pillow>=13.0.0"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

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(), 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.

Text from GitHub Advisory Database

Affected versions 12

  • Pillow, pip>= 11.0.0, < 13.0.0GitHub
  • Pillowaffected: >= 11.0.0, < 13.0.0CVE List
  • pillow, PyPIfrom 11.0.0, fixed in 13.0.0OSV
  • pillow, PyPI11.0.0OSV
  • pillow, PyPI11.1.0OSV
  • pillow, PyPI11.2.1OSV
  • pillow, PyPI11.3.0OSV
  • pillow, PyPI12.0.0OSV
  • pillow, PyPI12.1.0OSV
  • pillow, PyPI12.1.1OSV
  • pillow, PyPI12.2.0OSV
  • pillow, PyPI12.3.0OSV

References 6