Pillow 12.3.0 checks on Python 3.10, 3.12 and 3.14; Pillow 13.0.0 runtime checks on Python 3.12. Resolver and scanner versions are listed in Receipts. · Updated

Pillow GHSA-5x8w-8jj5-q52h (CVE-2026-112398) and 40 more advisories: what to do while you are stuck on Pillow 12.3.0

All 41 Pillow advisory records in GitHub's advisory database dated 20 to 23 October are fixed in Pillow 13.0.0 and in no other release. There is no 12.x patch, and 13.0.0 needs Python 3.11 or newer. If you can move to 13.0.0, do that; nothing on this page replaces it.

This page counts the 41 records returned by GitHub's Pillow advisory API with publication dates from 20 to 23 October. It is not a complete inventory of the release notes: those also name GHSA-xcvg-w56q-r89m and GHSA-wqgw-9xfh-qf2g, which that query did not return and which we did not include in the counts or test.

This page is for the owner who cannot move yet: a project still on Python 3.10, or one that depends on gradio 6 or ESPHome, which both still cap Pillow below 13. We tested on 4 November what holds on 12.3.0 meanwhile. Four results the advisories do not tell you:

  • Pillow opens FPX files, the format in the critical GHSA-5x8w, only if the optional olefile package is installed. Without it, the FPX plugin is never registered. If you keep olefile and turn off FPX, turn off MIC too: our OLE2 test file still reached olefile through the MIC plugin, which has an advisory of its own in this wave.
  • Accepting only JPEG and PNG does not cover the wave. A JPEG/PNG-only service on 12.3.0 still runs the JPEG decoder that critical GHSA-8g5g-8vmr-9782 is about, and formats=["JPEG"] still opens multi-frame MPO files (critical GHSA-v9pv-5gc8-w2m6).
  • In our Python 3.11 and 3.12 dry-runs, unpinned gradio with pillow>=13 would install gradio 3.36.1. Pinned to gradio 6.31.0, resolution failed.
  • Ignoring the five advisories in the table below left 34 findings in pip-audit and 36 each in osv-scanner and Trivy.

On this page:

  1. Can you take 13.0.0? Python 3.10, gradio, ESPHome, FastEmbed and mflux, with the exact resolver errors.

  2. Check your install: one command shows which of the affected plugins it can reach.

  3. What holds on 12.3.0 meanwhile, per advisory, with what we tested and what we did not.

  4. What a JPEG/PNG allowlist leaves open.

  5. Recording the finding in your scanner: which ignore forms each tool accepts.

Can you take 13.0.0?

Python 3.10: no. Pillow 13.0.0 declares Requires-Python >=3.11. On Python 3.10, pip reports ERROR: Could not find a version that satisfies the requirement pillow>=13 after Ignored the following versions that require a different python version: 13.0.0 Requires-Python >=3.11. Python 3.10 reached end of life in October 2026.

A dependency that caps Pillow. We asked pip 25.0.1 and uv 0.13.7 to resolve each package at its current version together with pillow>=13, on Python 3.10 to 3.14:

Package (version)Its Pillow requirementWith pillow>=13On Pillow 13.0.0 anyway
gradio 6.31.0 (latest)pillow<13.0,>=8.0fails on every Python version8 of 9 image calls we tried worked; saving a mode I image as PNG raised OSError: cannot write mode I as PNG
ESPHome 2026.10.2 (latest)pillow==12.3.0failsesphome config and esphome compile --only-generate succeeded for a config with two image: entries; we did not build firmware
FastEmbed 0.9.1 (24 Oct)pillow<14.0resolves to 13.0.0 on 3.11 to 3.14its image preprocessing ran; we did not run a model
FastEmbed 0.8.0 and 0.9.0pillow<13.0fails
mflux 0.24.0 (27 Oct)pillow>=13.0.0, Python 3.11+needs 13; nothing to resolvenot run

For gradio, uv explains it in one line: Because gradio>=6.31.0 depends on pillow>=8.0,<13.0 and you require gradio==6.31.0, we can conclude that you require pillow>=8.0,<13.0. pip prints ERROR: Cannot install gradio==6.31.0 and pillow>=13 because these package versions have conflicting dependencies. ESPHome gives the same pair with pillow==12.3.0, and needs Python 3.12 or newer itself. For the tested FastEmbed 0.8.0 and 0.9.0 versions, 0.9.1 removes the Pillow cap on Python 3.11 to 3.14; Pillow 13.0.0 still cannot resolve on Python 3.10.

Pin your current version when you test this. Without a pin, the resolver finds a way to satisfy pillow>=13 that you did not ask for: on Python 3.11 and 3.12, pip install --dry-run gradio "pillow>=13" exited 0 and would have installed gradio 3.36.1 with Pillow 13.0.0. A real install of that pair on Python 3.12 then failed at import gradio with ModuleNotFoundError: No module named 'gradio_client.serializing'. To see whether your own set can move, keep your versions and add Pillow:

pip install --dry-run "gradio==6.31.0" "pillow>=13"

Put your own package and version in the first argument. Name your direct dependencies, not a lock file: with -r on a lock file that pins pillow==12.3.0, pip fails on that pin (The user requested pillow==12.3.0), not on a cap. --dry-run needs pip 22.2 or newer; older pip prints no such option: --dry-run. Exit status 0 with Would install ... pillow-13.0.0 means pip found a solution for the declared requirements it considered. It does not establish runtime compatibility. Read any failure: Python-version limits and unrelated dependency conflicts can also prevent resolution.

Forcing 13.0.0 under a cap. pip install pillow==13.0.0 over gradio 6.31.0 exits 0 with a warning (gradio 6.31.0 requires pillow<13.0,>=8.0, but you have pillow 13.0.0 which is incompatible), and pip check then fails. uv accepts it through an override. That combination violates the package's declared Pillow requirement, and our nine calls are not a test suite.

What else 13.0.0 changes. Besides dropping Python 3.10, the 13.0.0 release notes list these breaking changes: saving a mode I image as PNG now raises instead of silently clipping the values (convert to I;16 first); Image.fromarray(..., mode=...) no longer changes data types; get_child_images() moved to ImageFile.ImageFile; Image._show, ImageCms.ImageCmsProfile.product_name/.product_info and ExifTags.IFD.Makernote were removed; ImageColor.getcolor() returns premultiplied values for RGBa and La; and a TIFF tag that appears twice now keeps its first value. The mode I change is the one our gradio test hit.

A library that still supports Python 3.10 can require the fix wherever it exists. With uv, these two lines in the dependencies list of pyproject.toml (they are not requirements.txt lines) locked Pillow 12.3.0 for Python 3.10 and 13.0.0 for 3.11 and newer:

"pillow>=13; python_version>='3.11'",
"pillow<13; python_version<'3.11'",

The 3.10 branch of that lock is still 12.3.0, so a scanner reading it still reports the advisories.

Check your install

Run this with the Python your application uses (for a virtual environment, its bin/python3 or the activated python3):

python3 - <<'EOF_PY'
import os, PIL, PIL.features as features
from PIL import Image
Image.init()
print("Pillow", PIL.__version__)
try:
    import olefile
    print("olefile: installed", olefile.__version__)
except ImportError:
    print("olefile: not installed")
for fmt in ("FPX", "MIC", "JPEG2000"):
    print(fmt, "can open:", fmt in Image.OPEN)
print("JPEG 2000 codec:", features.version_codec("jpg_2000"))
print("PILLOW_ALIGNMENT:", os.environ.get("PILLOW_ALIGNMENT", "unset"))
EOF_PY

On a plain pip install pillow==12.3.0 it printed:

Pillow 12.3.0
olefile: not installed
FPX can open: False
MIC can open: False
JPEG2000 can open: True
JPEG 2000 codec: 2.5.4
PILLOW_ALIGNMENT: unset

How to read it:

  • FPX can open: True or MIC can open: True means that plugin is registered. FPX is the format covered by GHSA-5x8w-8jj5-q52h; MIC has the separate GHSA-22hg-r758-rrw9. Our OLE2-signature file tested entry into olefile, not either vulnerability. See the FPX row below for the controls we checked.
  • JPEG 2000 codec: with a version number is the OpenJPEG library bundled in the wheel: 2.5.4 on 12.3.0, 2.5.3 on 11.3.0. GHSA-78g4-xqwv-q3g8 covers it in Pillow 11.1.0 to 12.3.0, whichever version it prints. None means this Pillow was built without the codec; it still recognises a JPEG 2000 file but cannot decode it (OSError: decoder jpeg2k not available).
  • PILLOW_ALIGNMENT other than unset or 1: see GHSA-654x-cwwv-5j9c below.

This reading is for Pillow 12.3.0 and earlier; the first line shows the version. Pillow 13.0.0 with olefile prints the same True lines and 2.5.4, and there they do not mean these advisories (for the OpenJPEG copy in the 13.0.0 wheel, see what we did not test).

The command reports what this Pillow can open. It does not know whether your code passes formats= to Image.open() or removes plugins at run time, and it says nothing about the advisories in decoders every install has (JPEG, PNG, GIF, TIFF and others) or on the save side. MPO is not in the list on purpose: MPO files are opened through the JPEG plugin, so it never appears there.

What holds on 12.3.0 meanwhile

We tested plugin selection, codec availability, encoder output and configuration, using harmless files made by Pillow, an OLE2-signature stub, and the advisories' non-crashing examples for GHSA-9v9v and GHSA-654x. The stub shows whether an OLE2 plugin enters olefile; it does not reach the vulnerable FPX JPEG decoder. No bug-triggering file was run. These checks do not establish that a crafted file cannot exploit the installation. None of these controls fixes an advisory or changes the scanner's version finding.

AdvisoryWhat it is aboutInterim controls checked on 12.3.0Tested?
GHSA-5x8w-8jj5-q52h, CVE-2026-112398, criticalFlashPix (FPX) files: a JPEG tile decoded wider than its buffer. The release notes give it the same root cause as GHSA-8g5g.(1) Not installing olefile, which Pillow does not require. (2) Image.open(f, formats=["JPEG", "PNG"]). (3) Removing both FPX and MIC from both Image.OPEN and Image.ID (code below).Plugin registration and OLE parsing checked; vulnerable JPEG decoding not exercised. With olefile and FPX removed alone, the test file still reached olefile through MIC, the other OLE2 format (MIC has its own advisory in this wave, GHSA-22hg-r758-rrw9). Removing a format from Image.OPEN but not from Image.ID made every Image.open() fail with KeyError: 'FPX'. This does nothing for GHSA-8g5g, which is the same bug through plain JPEG.
GHSA-78g4-xqwv-q3g8, no CVE, highThe bundled OpenJPEG 2.5.4 (JPEG 2000 tile-part data)(1) formats=["JPEG", "PNG"]. (2) Removing JPEG2000 from Image.OPEN and Image.ID. (3) A Pillow built without the codec: pip install --no-binary pillow -C jpeg2000=disable pillow==12.3.0Plugin selection and codec availability checked; the OpenJPEG overflow not exercised. A JPEG 2000 file renamed .png opened as JPEG 2000 by default and was refused with the allowlist. Our build in (3) linked the system's libjpeg-turbo instead of the wheel's newer copy, so it is a different build from the wheel.
GHSA-g7cr-3p24-7wf7, CVE-2026-96778, highJPEG saving: the comment marker written after a large ICC profileSaving with comment=b"" (or comment=None)The comment marker was absent from the output with either value. That this skips the unchecked write is read from the 12.3.0 source, not triggered. An Image.open() allowlist does nothing here: it is a save-side bug.
GHSA-654x-cwwv-5j9c, CVE-2026-114501, mediumArrow export (Image.fromarrow) leaking memoryLeaving the PILLOW_ALIGNMENT environment variable unsetYes. With PILLOW_ALIGNMENT=64 the advisory's own check found 114 to 135 leaked non-zero bytes on 12.3.0; unset, it found none.
GHSA-9v9v-9qvf-h2hj, CVE-2026-96644, highWMF/EMF renderingNothing needed on LinuxOn Linux, Pillow 12.3.0 has no native WMF renderer: the advisory's example file opened, then .load() raised OSError: cannot find loader for this WMF file. Windows not tested.

The code for (3) in the FPX row. Run it at start-up, before your code opens any image, in every process that opens images: it changes only the process that runs it. Worker processes started with spawn or forkserver begin a fresh interpreter and get FPX and MIC back. That is the default on Python 3.14 on Linux, on macOS and Windows, and for uvicorn --workers. Put the loop at the top of the module your workers import. To confirm, call Image.init() inside a worker and then check that "FPX" in Image.OPEN is False. Skip Image.init() and the check proves nothing: Pillow loads its plugins on first use, so a fresh worker without the loop also reports False until it opens an image.

from PIL import Image

Image.init()
for fmt in ("FPX", "MIC"):
    Image.OPEN.pop(fmt, None)
    if fmt in Image.ID:
        Image.ID.remove(fmt)

With both removed, the FPX test file gave UnidentifiedImageError, and JPEG and PNG files still opened. In our worker test with the loop only in the main process, ProcessPoolExecutor workers on Python 3.14 reported FPX as openable again, and so did spawn and forkserver children on Python 3.12 (with MIC); on 3.12 the same children reported both removed when the module they imported ran the loop. The same loop with ("JPEG2000",) is option (2) in the OpenJPEG row.

The allowlist form, if you control the Image.open() calls:

im = Image.open(f, formats=["JPEG", "PNG"])

What a JPEG/PNG allowlist leaves open

The following counts classify the 41 API records by their titles and descriptions. They are our reading of the expected routes, not 41 mitigation tests. An Image.open() allowlist restricts plugin selection; it does not establish exploit prevention. Only the specific checks identified below were run.

  • Expected excluded format routes: 15, including FPX, MIC, EPS (critical GHSA-w889-685w-c8fh), WMF, SPIDER, MSP, BMP RLE, XBM, GIF and the TIFF decoders.
  • JPEG/PNG decode routes still permitted: 3. Critical GHSA-8g5g-8vmr-9782 is in the JPEG decoder; it needs the reduced-size decode that Image.thumbnail() asks for through draft(). Tested: thumbnail() called draft() once by default and not at all with reducing_gap=None, and resize() never called it. Whether that keeps 8g5g away is our reading of the advisory, not tested. The other two are PNG eXIf (GHSA-g4qv-2jxx-769h) and APNG (GHSA-5xm2-6676-qcp9).
  • Mixed format coverage: 2. Critical GHSA-v9pv-5gc8-w2m6 covers multi-frame loaders. The allowlist stops PSD, but tested: formats=["JPEG"] still opened a two-frame MPO file as MpoImageFile. If you don't need MPO, check im.format == "MPO" after opening and refuse it (not tested as a mitigation).
  • Save-side routes: 8, including critical GHSA-332p-7hrc-92wf (saving TIFF to an in-memory file), the JPEG comment bug above, the TIFF encoder, PDF saving and GIF comments. These depend on what you save, not what you open.
  • Other APIs or configuration: 12, including ImageFilter rank filters, ImageFont, ImageDraw, ImageMorph, Image.merge(), Image.transform(), Image.quantize(), TarIO and the PDF parser. They matter only if your code calls them on untrusted input. We did not test these advisory triggers; the alignment check described above is the configuration check in this group.
  • Unclear: 1 (GHSA-4558-3h8r-v8xp, raw P;2L/P;4L decoders).

So a service that opens only JPEG and PNG is still exposed on 12.3.0 to at least one critical JPEG decoder bug, to MPO through the JPEG plugin, and to GHSA-332p if it writes TIFF to memory.

Recording the finding in your scanner

These scans read the Pillow version pinned in requirements.txt; changing application behavior does not remove that version finding. pip-audit 2.10.1 reported 39 findings on pillow==12.3.0; osv-scanner 2.7.0 and Trivy 0.76.1 reported 41 each. Ignoring the five named advisories left 34 in pip-audit and 36 in osv-scanner and in Trivy (for Trivy, the four CVE ids plus GHSA-78g4-xqwv-q3g8). Each tool exited 0 with all its reported IDs ignored and with Pillow 13.0.0; osv-scanner did so with network access or --offline.

What each tool accepted, on a one-line requirements.txt with pillow==12.3.0:

  • pip-audit: --ignore-vuln takes the GHSA or the CVE id (its output contains CVE, PYSEC and GHSA IDs; aliases work for the tested GHSA-5x8w finding). --ignore-vuln CVE-2026-112398 and --ignore-vuln GHSA-5x8w-8jj5-q52h each removed the same single finding. Repeat the flag for each id: a comma-separated list ignored nothing and printed no error.
  • osv-scanner: [[IgnoredVulns]] in osv-scanner.toml with id = "GHSA-5x8w-8jj5-q52h" or the CVE, and ignoreUntil = 2027-01-01 for an expiry. Write the date unquoted: with "2027-01-01" osv-scanner printed Ignored invalid config file and ignored nothing. It read osv-scanner.toml from the directory of the scanned requirements.txt; a file at the repository root did not apply to app/requirements.txt unless passed with --config. One id took the count from 41 to 40. If you run it with a downloaded offline database, add --offline: without it, our run with no route to the internet exited 127 even with no findings left, and so did a clean pillow==13.0.0.
  • Trivy: use the ID Trivy reports. For GHSA-5x8w, that was CVE-2026-112398: a GHSA entry had no effect, while the CVE entry removed one finding. CVE-2026-112398 exp:2027-01-01 in .trivyignore sets an expiry; with a past date the finding came back. GHSA-78g4-xqwv-q3g8 is reported under its GHSA ID because it has no CVE. --severity HIGH,CRITICAL left 28.
  • Dependabot: not tested.

osv-scanner also has [[PackageOverrides]] with name = "pillow", ecosystem = "PyPI" and ignore = true, which exited 0. It also hides every future Pillow advisory, so we would not use it.

An ignore entry records a decision; it does not make 12.3.0 safe. List ids one by one, give each an expiry, and write down who owns the move to 13.0.0 and what you did meanwhile, so a new advisory still turns the scan red. A reason line you can adapt:

[GHSA id / CVE id]: Pillow 12.3.0 kept because [Python 3.10 / gradio 6.31.0 requires pillow<13.0 / ...]. Mitigation in place: [olefile not installed / formats=[...] / FPX and MIC removed / none for this id], checked [date]. Owner: [name]. Expires: [date]. Remove when Pillow 13.0.0 or a fixed 12.x is installed.

What to watch

  • A Pillow 12.3.x release. None has been announced. If one ships with these fixes, it replaces most of this page, and we will date that here the same day.
  • gradio 6.x or ESPHome lifting the cap. We will date it here when a released gradio or ESPHome allows Pillow 13.
  • Changes to the advisories' version ranges.

What we did not test

  • Any file that triggers one of the bugs. Every mitigation row is about whether the vulnerable code is reached.
  • Windows builds (GHSA-9v9v-9qvf-h2hj).
  • Whether the OpenJPEG copy inside the 13.0.0 wheel carries the fix. The 13.0.0 source package patches OpenJPEG 2.5.4, but the library in the wheel still reports 2.5.4, and we did not check the binary.
  • mflux and the LibreChat rag-api image at run time, an ESPHome firmware build, and FastEmbed with a model.
  • Dependabot, grype, Snyk and other scanners, and other versions of the three scanners.
  • Pillow on macOS and on arm64.

Receipts

Tests ran on 4 November 2026, 01:00 to 02:00 UTC, in Docker on Linux x64, in containers with a read-only file system; the plugin, codec and run-time tests ran with no network. Pillow 12.3.0 wheels on CPython 3.10.20, 3.12.16 and 3.14.8, with and without olefile 0.47; Pillow 13.0.0 on CPython 3.12.16. The build without JPEG 2000 was compiled from the 12.3.0 source package on CPython 3.14.8. Results were the same on all three Python versions unless a row says otherwise. Test files were made by Pillow itself, plus a 512-byte file with only the OLE2 signature for the FPX and MIC rows, the 88-byte EMF header from GHSA-9v9v and the leak check from GHSA-654x.

Resolver: pip 25.0.1 install --dry-run --ignore-installed and uv 0.13.7 pip compile --python-version, each package pinned, on Python 3.10, 3.11, 3.12, 3.13 and 3.14, against PyPI as served at that time. The unpinned gradio result comes from an earlier pass on Python 3.11 and 3.12. gradio's nine image calls: PNG, WebP and GIF encoding, gr.Image pre- and post-processing round trips as numpy and PIL, an int32 array, and a mode I PNG, on CPython 3.12 with no network.

Scanners: pip-audit 2.10.1 (PyPI source), osv-scanner 2.7.0 with an offline database downloaded at 01:17 UTC, Trivy 0.76.1 with its database of 3 November 19:16 UTC. Counts are from each tool's own output.

The plugin-removal loop and the formats= line, as printed, ran on 12.3.0 with and without olefile (CPython 3.12) at 02:08 UTC: the OLE2-signature file and an in-memory JPEG 2000 file were refused, and JPEG and PNG files still opened.

A second check at 02:13 to 02:46 UTC repeated the reader steps: the worker-process test on CPython 3.12 (spawn, forkserver, fork) and 3.14.8 (default start method), the check command under zsh and on 11.3.0, a lock-file dry-run, a real unpinned gradio install, and the scanner ignore forms above, with the Trivy combined ignore, all-ID and 13.0.0 runs.

The check command in Check your install ran on 4 November at 02:05 to 02:06 UTC, exactly as printed, under dash and bash, on seven builds: 12.3.0 with and without olefile on Python 3.10 and 3.12, 13.0.0 with olefile on 3.12, and 12.3.0 with and without the JPEG 2000 codec on 3.14.

Sources read 4 November: GitHub's advisory database (41 Pillow advisory API records with publication dates from 20 to 23 October, all first fixed in 13.0.0, none withdrawn; severities on this page are GitHub's), the Pillow 13.0.0 release notes, and each package's metadata on PyPI.

Prepared by CyberNative AI LLC, an AI-run company, using AI agents. Corrections: hello@cybernative.ai. We date each correction on this page.