critical

CVE

CVE-2026-96639, CVE-2026-112398, CVE-2026-112403, CVE-2026-112393, CVE-2026-112900, CVE-2026-112902, CVE-2026-96778, CVE-2026-96644, CVE-2026-96637, CVE-2026-96643

CWE

CWE-122, CWE-787, CWE-125, CWE-88, CWE-78, CWE-1395

Affected Surface

  • PyPI package `Pillow` (`pillow`) versions `< 13.0.0`, including `11.0.0` through `12.3.0` for `CVE-2026-96639` and `2.0.0` through `12.3.0` for `CVE-2026-112393`
  • All prebuilt `Pillow` wheels from `11.1.0` through `12.3.0`, which bundle `libopenjp2` `2.5.3` or `2.5.4` without upstream commit `91d08b11` (`GHSA-78g4-xqwv-q3g8`)
  • Python web applications, thumbnailers, document pipelines, and AI vision or multimodal services that call `Image.open()`, `ImageSequence.Iterator`, `im.thumbnail()`, or `im.save(BytesIO(), 'TIFF')` on untrusted uploads
  • Linux, macOS, and Windows hosts with `olefile` installed (enabling automatic FlashPix and MIC detection) or Ghostscript `gs` on `PATH` (enabling EPS rendering)

TL;DR

Between 19 and 22 October 2026, NVD, oss-security, and the GitHub Advisory Database published CVE assignments and technical advisories for more than 30 security flaws fixed in Pillow 13.0.0. Five advisories carry Critical ratings (CVSS 9.2 to 9.8), and four include working command-execution proofs against realistic Python image-processing code paths:

  1. CVE-2026-96639 (GHSA-v9pv-5gc8-w2m6, CVSS 9.8 Critical): Multi-frame loaders (MPO, PSD, DCX, MIC) can switch from a narrow 1-byte pixel mode (L) on frame 0 to a wider mode (RGB or CMYK) on frame 1 while keeping the frame 0 native ImagingCore allocation in self._im. Decoding frame 1 writes wider pixels past the narrow buffer, and the reporter demonstrated command execution through the MPO/JPEG ImageSequence.Iterator path.
  2. CVE-2026-112398 (GHSA-5x8w-8jj5-q52h, Critical) and CVE-2026-112403 (GHSA-8g5g-8vmr-9782, CVSS 9.2 Critical): ImagingJpegDecode() never verified that libjpeg’s output scanline width and component count fit inside state->buffer. In FlashPix (FpxImagePlugin, enabled whenever olefile is installed), an unmapped "LA" rawmode falls back to JCS_UNKNOWN and writes 4-component scanlines into a 2-byte-per-pixel buffer, hijacking PyBuffer_Release for one-file Image.open().load() command execution. In standard JPEG, calling im.thumbnail() (which invokes draft()) on an 8-bit lossless (SOF3) JPEG against libjpeg-turbo 3.x writes unscaled scanlines into a downscaled row buffer.
  3. CVE-2026-112393 (GHSA-332p-7hrc-92wf, CVSS 9.2 Critical): Saving a compressed TIFF to an in-memory buffer such as io.BytesIO() routes libtiff through _tiffWriteProc(), which sized realloc() from state->size + size instead of state->loc + size. After libtiff seeks past the IFD directory, the next tag write overflows the heap buffer and can overwrite tif->tif_cleanup with system(). Even without crafted tags, unwritten alignment holes leak stale heap bytes into the returned TIFF.
  4. CVE-2026-112900 (GHSA-w889-685w-c8fh, CVSS 9.2 Critical): EpsImagePlugin.Ghostscript() passed fp.name straight after -f without converting it to an absolute path. A caller-controlled filename starting with - or @ injects Ghostscript flags or an @argfile (-sOutputFile=%pipe%...) and reaches OS command execution even with -dSAFER enabled.
  5. GHSA-78g4-xqwv-q3g8 (CVSS 8.1 High): Every prebuilt Pillow wheel from 11.1.0 through 12.3.0 bundles libopenjp2 2.5.3 or 2.5.4, which contains an unpatched heap-buffer-overflow write in opj_j2k_read_sod reachable from Image.open().load() regardless of the uploaded file’s extension.

Upgrade to Pillow>=13.0.0 immediately. Note two operational catches before deploying: Pillow 13.0.0 drops Python 3.10 (requires_python >= 3.11) with no 12.x backport, and Image.open() sniffs magic bytes across all registered codecs unless you pass an explicit formats=[...] allowlist.

Affected packages and projects

EcosystemPackageCVE / GHSAAffected versionsFixed versionVulnerable component and trigger
PyPIPillowCVE-2026-96639 (GHSA-v9pv-5gc8-w2m6, CVSS 9.8)>= 11.0.0, < 13.0.013.0.0MpoImagePlugin, PsdImagePlugin, DcxImagePlugin, MicImagePlugin seek() retaining self._im across mode or size transitions
PyPIPillowCVE-2026-112398 (GHSA-5x8w-8jj5-q52h, Critical)< 13.0.013.0.0FpxImagePlugin + ImagingJpegDecode (src/libImaging/JpegDecode.c) decoding 4-channel JCS_UNKNOWN JPEG tiles into a 2-byte "LA" row buffer when olefile is installed
PyPIPillowCVE-2026-112403 (GHSA-8g5g-8vmr-9782, CVSS 9.2)<= 12.3.013.0.0JpegImagePlugin.draft() + ImagingJpegDecode with libjpeg-turbo >= 3.0 decoding lossless SOF3 JPEGs during im.thumbnail()
PyPIPillowCVE-2026-112393 (GHSA-332p-7hrc-92wf, CVSS 9.2)>= 2.0.0, < 13.0.013.0.0_tiffWriteProc() in src/libImaging/TiffDecode.c when saving compressed TIFFs to io.BytesIO() or any stream without fileno()
PyPIPillowCVE-2026-112900 (GHSA-w889-685w-c8fh, CVSS 9.2)>= 2.6.0, < 13.0.013.0.0EpsImagePlugin.Ghostscript() passing unnormalized fp.name (- or @ prefix) to gs
PyPIPillow (wheels)GHSA-78g4-xqwv-q3g8 (CVSS 8.1)>= 11.1.0, < 13.0.013.0.0Bundled pillow.libs/libopenjp2-*.so.2.5.4 missing upstream opj_j2k_read_sod bounds check (91d08b11)
PyPIPillowCVE-2026-96778 (GHSA-g7cr-3p24-7wf7, High)< 13.0.013.0.0JPEG encoder heap overflow writing COM comment after an ICC profile fills the buffer
PyPIPillowCVE-2026-112395, CVE-2026-112404, CVE-2026-112896, CVE-2026-112897, CVE-2026-112902 (High)< 13.0.013.0.0TiffDecode.c out-of-bounds reads, uninitialized memory disclosure, and encoder tag crashes

Technical analysis

1. CVE-2026-96639: multi-frame seek() reuses narrow native ImagingCore storage for wider frames

When Pillow decodes a frame, ImageFile.load_prepare() allocates the underlying C ImagingCore pixel buffer (self._im) only if self._im is None. In Pillow 11.0.0 through 12.3.0, four multi-frame plugins (MpoImagePlugin, PsdImagePlugin, DcxImagePlugin, and MicImagePlugin) updated self._mode and self.tile during seek() without clearing self._im or calling Image.Image.seek().

If frame 0 is a grayscale L image (1 byte per pixel) of dimensions (W \times H) and frame 1 has the same (W \times H) dimensions but uses RGB or CMYK (4 bytes per pixel in ImagingCore), load_prepare() sees self._im is not None and hands the 1-byte-per-pixel native buffer to the frame 1 decoder:

from io import BytesIO
from PIL import Image, ImageSequence

image = Image.open(BytesIO(upload_bytes))
for frame in ImageSequence.Iterator(image):
    frame.load()  # Frame 0 allocates L; Frame 1 decodes CMYK into the L buffer

Researcher Rotem Kama demonstrated that a two-frame MPO file (L followed by CMYK) causes both a native heap out-of-bounds write and an over-read, and achieved 10-for-10 marker command execution against a local web upload service running Python 3.12.3 and 3.13.14 with Pillow 12.3.0.

Upstream commit 78d97d994579 (PR #10105) fixes all four loaders by routing their seek() methods through Image.Image.seek(), which invalidates self._im whenever size or mode changes:

--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -2746,10 +2746,16 @@ def seek(self, frame: int) -> None:
-        if frame != 0:
+        if frame != self.tell():
+            # The file handler likely did not override this method
             msg = "no more images in file"
             raise EOFError(msg)
 
+        if self._im is not None and (
+            self.im.size != self.size or self.im.mode != self.mode
+        ):
+            self._im = None

2. CVE-2026-112398 and CVE-2026-112403: ImagingJpegDecode trusts tile extents over libjpeg output geometry

Before 13.0.0, _setimage() in src/decode.c allocated the single-row decode buffer state->buffer from the tile extents and unpacker bit depth (state->bytes = (state->bits * state->xsize + 7) / 8), and ImagingJpegDecode() in src/libImaging/JpegDecode.c called jpeg_start_decompress() followed immediately by jpeg_read_scanlines(&context->cinfo, &state->buffer, 1) without checking how many bytes libjpeg was actually going to write per scanline.

Two independent advisories reach that unchecked jpeg_read_scanlines() call from different angles:

  • FlashPix JPEG tiles (CVE-2026-112398 / GHSA-5x8w-8jj5-q52h, reported by Alex Bouffard): Whenever olefile is installed in the Python environment, Image.open() automatically recognizes OLE2 compound documents (D0 CF 11 E0...) as FlashPix (FpxImagePlugin) or MIC regardless of filename. In FpxImagePlugin.py, color ID (0x00018000, 0x00017FFE) maps to ("RGBA", "LA"), sizing state->buffer for "LA" (2 * width bytes per row). When JpegDecode.c inspects the "LA" rawmode, it does not recognize the string and falls back to JCS_UNKNOWN, telling libjpeg to emit raw components from the embedded JPEG’s SOF header. A 4-component embedded JPEG writes 4 * width bytes into the 2 * width buffer. Because the overflow data is the decoded pixel stream, Bouffard used an iterative decode-feedback loop (make_payload.py) to overwrite an adjacent Python bytes object’s ob_type pointer with a fake PyTypeObject / PyBufferProcs vtable, redirecting PyBuffer_Release (bf_releasebuffer) to system@plt when Image.open(path).load() finishes.
  • Lossless SOF3 JPEGs in im.thumbnail() (CVE-2026-112403 / GHSA-8g5g-8vmr-9782, reported by Anthropic and Ada Logics): Calling im.thumbnail((256, 256)) invokes JpegImageFile.draft(), which requests 1/2, 1/4, or 1/8 scaling (cinfo.scale_denom = s) and shrinks state->xsize to (\lceil W / s \rceil). However, libjpeg-turbo 3.x (bundled in all official Pillow wheels) supports 8-bit spatial lossless (SOF3, marker 0xFFC3) JPEGs and silently ignores scale_denom for lossless streams. jpeg_read_scanlines() writes full-width (W)-pixel rows into the (\lceil W / s \rceil) buffer, giving a byte-exact heap overflow of ((W - \lceil W / s \rceil)) bytes on every row.

Upstream commit 242a059aedc1 (PR #10131) closes both paths by checking cinfo.output_width and cinfo.output_components right after jpeg_start_decompress():

--- a/src/libImaging/JpegDecode.c
+++ b/src/libImaging/JpegDecode.c
@@ -241,6 +241,17 @@ ImagingJpegDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t by
                 break;
             }
 
+            /* The scanline buffer was sized from the tile extents and the
+               unpacker, not from the JPEG header, so make sure the lines
+               libjpeg is about to produce actually fit in it. */
+            if (context->cinfo.output_width > (JDIMENSION)state->xsize ||
+                (size_t)context->cinfo.output_components * context->cinfo.output_width >
+                    (size_t)state->bytes) {
+                jpeg_destroy_decompress(&context->cinfo);
+                state->errcode = IMAGING_CODEC_BROKEN;
+                return -1;
+            }
+
             state->state++;
             /* fall through */

3. CVE-2026-112393: _tiffWriteProc heap overflow and heap-memory leak when saving TIFFs to BytesIO

When an application saves a compressed TIFF to a file-like object that has no OS file descriptor (io.BytesIO, an HTTP response stream, or an S3/GCS upload buffer), Pillow opens libtiff via TIFFClientOpen() against its custom in-memory callbacks _tiffWriteProc() and _tiffSeekProc() in src/libImaging/TiffDecode.c.

While writing the TIFF directory (IFD), libtiff seeks past the current end of the buffer (state->loc = eof + ifd_size) and writes out-of-line tag values (such as ImageDescription, DocumentName, XMP, or icc_profile) at that forward offset. Since Pillow 2.0.0 (2013), _tiffWriteProc() grew its allocation against size + state->size instead of size + state->loc, and never zeroed the newly realloc()’d tail:

--- a/src/libImaging/TiffDecode.c
+++ b/src/libImaging/TiffDecode.c
@@ -67,11 +67,13 @@ _tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) {
+    // libtiff may have seeked past the end of the buffer, e.g. when writing the
+    // directory, so the required size is based on the current location.
     to_write = MIN(size, state->size - (tsize_t)state->loc);
     if (state->flrealloc && size > to_write) {
         tdata_t new_data;
         tsize_t newsize = state->size;
-        while (newsize < (size + state->size)) {
+        while (newsize < (size + (tsize_t)state->loc)) {
             if (newsize > INT_MAX - 64 * 1024) {
                 return 0;
             }
@@ -84,9 +86,20 @@ _tiffWriteProc(thandle_t hdata, tdata_t buf, tsize_t size) {
             // fail out
             return 0;
         }
+        // zero the new space, so that any bytes libtiff seeks over without
+        // writing are 0 in the output, as they would be in a file
+        memset((UINT8 *)new_data + state->size, 0, newsize - state->size);
         state->data = new_data;
         state->size = newsize;
         to_write = size;
+    } else if (to_write < 0) {
+        TIFFError(
+            "_tiffWriteProc",
+            "Invalid Write at loc %" PRIu64 ", size: %" PRIu64,
+            state->loc,
+            (toff_t)state->size
+        );
+        return 0;
     }
 
     _TIFFmemcpy((UINT8 *)state->data + state->loc, buf, to_write);

When the compressed pixel strip ends near a 64 KiB boundary and is followed by a ~64 KiB tag or ICC profile (which survives im.copy(), im.resize(), and im.thumbnail() inside im.info["icc_profile"]), _TIFFmemcpy writes up to IFD_size bytes (12 bytes per IFD entry; 2,384 bytes in the advisory’s 331-entry test) past the end of the heap chunk. Using repeated IFD tags to shape glibc’s free lists, the reporters placed the 128 KiB sink buffer directly below libtiff’s own TIFF struct (malloc(1313)), overwrote tif->tif_cleanup at +0x3f8 with system, and achieved remote command execution on Image.open(BytesIO(body)).save(BytesIO(), "TIFF") (commit 11582e5ffbdc).

4. CVE-2026-112900 and GHSA-78g4-xqwv-q3g8: Ghostscript @argfile injection and bundled libopenjp2 wheels

Two more flaws in the October batch bypass application-layer upload filters:

  • Ghostscript argument injection (CVE-2026-112900 / GHSA-w889-685w-c8fh, reported by Chanu Kang at KAIST): In src/PIL/EpsImagePlugin.py, Ghostscript() placed infile = fp.name directly after "-f" in the gs argument list. Even though Pillow passes -dSAFER, Ghostscript still parses any argument after -f that starts with - as a flag and any argument starting with @ as an @argfile containing newline-separated options. If an upload handler preserves a user-supplied filename (or allows @<path> referencing a second uploaded file containing -sOutputFile=%pipe%/bin/id), Image.open(name).load() executes arbitrary shell commands on Ghostscript 10.02.1 through 10.07.1. Commit a2c363141da3 (PR #10090) fixes this with infile = os.path.abspath(fp.name).
  • Bundled libopenjp2 2.5.4 (GHSA-78g4-xqwv-q3g8): Every prebuilt Pillow wheel from 11.1.0 through 12.3.0 ships pillow.libs/libopenjp2-*.so.2.5.4 (or 2.5.3). In opj_j2k_read_sod (src/lib/openjp2/j2k.c), a JPEG 2000 codestream with a valid TLM marker and TNsot = 0 in its SOT markers uses the one-byte TPsot field (l_current_tile_part) to index tp_index[] without a bounds check, walking up to 5,856 bytes past a 24-byte heap allocation and overwriting adjacent opj_event_mgr_t function pointers. Because OpenJPEG has not cut a release since v2.5.4 (September 2025), host OS patches do not fix virtualenv wheels; Pillow 13.0.0 (commit 5f8114c8096b) backports upstream OpenJPEG commit 91d08b11 directly into the wheel build.

Detection and remediation

1. Find vulnerable Pillow installations and check bundled codecs

Inspect virtualenvs, lockfiles, and container images for any Pillow version below 13.0.0, and check whether olefile or gs is present on the host:

# Check installed Pillow version, bundled libjpeg / openjpeg versions, and optional RCE dependencies
python3 -c '
import shutil, PIL
from PIL import Image, features
print("Pillow:", PIL.__version__)
print("jpg codec:", features.version_codec("jpg"))
print("jpg_2000 codec:", features.version_codec("jpg_2000"))
try:
    import olefile
    print("olefile installed:", olefile.__version__)
except ImportError:
    print("olefile installed: False")
print("ghostscript binary:", shutil.which("gs") or shutil.which("gswin64c"))
'

# Search lockfiles and manifests across the repository
rg -n -i '"?pillow"?\s*([~=!<>]=|:)' \
  requirements*.txt pyproject.toml uv.lock poetry.lock Pipfile.lock setup.cfg 2>/dev/null

2. Upgrade to Pillow 13.0.0 (and handle the Python 3.11 floor)

  1. Upgrade to Pillow>=13.0.0:
    pip install --upgrade "Pillow>=13.0.0"
    Note that Pillow 13.0.0 drops Python 3.10 support (requires_python >= 3.11) and there is no 12.x security backport. Services still pinned to Python 3.10 must either upgrade the runtime to Python 3.11+ or apply the five C and Python patches above to a source build of 12.3.0.
  2. Restrict Image.open() codecs with formats=[...]: Because Image.open() sniffs magic bytes rather than file extensions or Content-Type headers, an endpoint intended only for PNG and JPEG avatars will happily decode FlashPix (OLE2), MPO, PSD, EPS, or JPEG 2000 payloads unless you restrict the opener:
    from PIL import Image
    
    with Image.open(stream, formats=["JPEG", "PNG", "WEBP"]) as im:
        im.load()
  3. If you cannot upgrade immediately, apply targeted mitigations:
    • Uninstall olefile (pip uninstall olefile) if your application does not need FlashPix or Microsoft Image Composer (MIC) files; this disables automatic FpxImagePlugin and MicImagePlugin detection (CVE-2026-112398).
    • Remove the ghostscript package (apt-get purge ghostscript) from image-processing containers that do not render EPS files, and always pass os.path.abspath(path) or a BytesIO stream to Image.open() (CVE-2026-112900).
    • Call im.load() before im.thumbnail() (or pass reducing_gap=None) to skip JpegImageFile.draft() (CVE-2026-112403).
    • Save compressed TIFFs to a temporary file (tempfile.NamedTemporaryFile) rather than io.BytesIO() so libtiff uses TIFFFdOpen instead of _tiffWriteProc (CVE-2026-112393).

Timeline

  • 2026-02-10: OpenJPEG merges commit 91d08b11 fixing the opj_j2k_read_sod heap overflow on master, without cutting a new release tag after v2.5.4.
  • 2026-10-16: Pillow 13.0.0 is uploaded to PyPI (22:15 UTC) with source and wheel fixes across JPEG, TIFF, OpenJPEG, EPS, PDF, and multi-frame plugins.
  • 2026-10-17: The Pillow maintainers publish initial repository security advisories before CVE assignment.
  • 2026-10-19 to 2026-10-21: NVD publishes CVE records for the batch, including CVE-2026-96639 (CVSS 9.8), CVE-2026-112900 (CVSS 9.2), CVE-2026-112393 (CVSS 9.2), CVE-2026-112398, and CVE-2026-112403 (CVSS 9.2).
  • 2026-10-21: Alan Coopersmith posts the consolidated CVE-to-GHSA mapping to oss-security (2026/10/21/24), and GitHub reviews and publishes the first wave of global advisories.
  • 2026-10-22: GitHub Advisory Database publishes the remaining reviewed advisories (GHSA-v9pv-5gc8-w2m6, GHSA-78g4-xqwv-q3g8, GHSA-9v9v-9qvf-h2hj, GHSA-g7cr-3p24-7wf7, GHSA-g842-9rc4-69pw), and OX Security publishes its analysis of the critical RCE and heap-overflow paths.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References