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:
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 (RGBorCMYK) on frame 1 while keeping the frame 0 nativeImagingCoreallocation inself._im. Decoding frame 1 writes wider pixels past the narrow buffer, and the reporter demonstrated command execution through the MPO/JPEGImageSequence.Iteratorpath.CVE-2026-112398(GHSA-5x8w-8jj5-q52h, Critical) andCVE-2026-112403(GHSA-8g5g-8vmr-9782, CVSS 9.2 Critical):ImagingJpegDecode()never verified thatlibjpeg’s output scanline width and component count fit insidestate->buffer. In FlashPix (FpxImagePlugin, enabled wheneverolefileis installed), an unmapped"LA"rawmode falls back toJCS_UNKNOWNand writes 4-component scanlines into a 2-byte-per-pixel buffer, hijackingPyBuffer_Releasefor one-fileImage.open().load()command execution. In standard JPEG, callingim.thumbnail()(which invokesdraft()) on an 8-bit lossless (SOF3) JPEG againstlibjpeg-turbo3.x writes unscaled scanlines into a downscaled row buffer.CVE-2026-112393(GHSA-332p-7hrc-92wf, CVSS 9.2 Critical): Saving a compressed TIFF to an in-memory buffer such asio.BytesIO()routeslibtiffthrough_tiffWriteProc(), which sizedrealloc()fromstate->size + sizeinstead ofstate->loc + size. Afterlibtiffseeks past the IFD directory, the next tag write overflows the heap buffer and can overwritetif->tif_cleanupwithsystem(). Even without crafted tags, unwritten alignment holes leak stale heap bytes into the returned TIFF.CVE-2026-112900(GHSA-w889-685w-c8fh, CVSS 9.2 Critical):EpsImagePlugin.Ghostscript()passedfp.namestraight after-fwithout 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-dSAFERenabled.GHSA-78g4-xqwv-q3g8(CVSS 8.1 High): Every prebuiltPillowwheel from11.1.0through12.3.0bundleslibopenjp22.5.3or2.5.4, which contains an unpatched heap-buffer-overflow write inopj_j2k_read_sodreachable fromImage.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
| Ecosystem | Package | CVE / GHSA | Affected versions | Fixed version | Vulnerable component and trigger |
|---|---|---|---|---|---|
| PyPI | Pillow | CVE-2026-96639 (GHSA-v9pv-5gc8-w2m6, CVSS 9.8) | >= 11.0.0, < 13.0.0 | 13.0.0 | MpoImagePlugin, PsdImagePlugin, DcxImagePlugin, MicImagePlugin seek() retaining self._im across mode or size transitions |
| PyPI | Pillow | CVE-2026-112398 (GHSA-5x8w-8jj5-q52h, Critical) | < 13.0.0 | 13.0.0 | FpxImagePlugin + ImagingJpegDecode (src/libImaging/JpegDecode.c) decoding 4-channel JCS_UNKNOWN JPEG tiles into a 2-byte "LA" row buffer when olefile is installed |
| PyPI | Pillow | CVE-2026-112403 (GHSA-8g5g-8vmr-9782, CVSS 9.2) | <= 12.3.0 | 13.0.0 | JpegImagePlugin.draft() + ImagingJpegDecode with libjpeg-turbo >= 3.0 decoding lossless SOF3 JPEGs during im.thumbnail() |
| PyPI | Pillow | CVE-2026-112393 (GHSA-332p-7hrc-92wf, CVSS 9.2) | >= 2.0.0, < 13.0.0 | 13.0.0 | _tiffWriteProc() in src/libImaging/TiffDecode.c when saving compressed TIFFs to io.BytesIO() or any stream without fileno() |
| PyPI | Pillow | CVE-2026-112900 (GHSA-w889-685w-c8fh, CVSS 9.2) | >= 2.6.0, < 13.0.0 | 13.0.0 | EpsImagePlugin.Ghostscript() passing unnormalized fp.name (- or @ prefix) to gs |
| PyPI | Pillow (wheels) | GHSA-78g4-xqwv-q3g8 (CVSS 8.1) | >= 11.1.0, < 13.0.0 | 13.0.0 | Bundled pillow.libs/libopenjp2-*.so.2.5.4 missing upstream opj_j2k_read_sod bounds check (91d08b11) |
| PyPI | Pillow | CVE-2026-96778 (GHSA-g7cr-3p24-7wf7, High) | < 13.0.0 | 13.0.0 | JPEG encoder heap overflow writing COM comment after an ICC profile fills the buffer |
| PyPI | Pillow | CVE-2026-112395, CVE-2026-112404, CVE-2026-112896, CVE-2026-112897, CVE-2026-112902 (High) | < 13.0.0 | 13.0.0 | TiffDecode.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): Wheneverolefileis installed in the Python environment,Image.open()automatically recognizes OLE2 compound documents (D0 CF 11 E0...) as FlashPix (FpxImagePlugin) orMICregardless of filename. InFpxImagePlugin.py, color ID(0x00018000, 0x00017FFE)maps to("RGBA", "LA"), sizingstate->bufferfor"LA"(2 * widthbytes per row). WhenJpegDecode.cinspects the"LA"rawmode, it does not recognize the string and falls back toJCS_UNKNOWN, tellinglibjpegto emit raw components from the embedded JPEG’sSOFheader. A 4-component embedded JPEG writes4 * widthbytes into the2 * widthbuffer. Because the overflow data is the decoded pixel stream, Bouffard used an iterative decode-feedback loop (make_payload.py) to overwrite an adjacent Pythonbytesobject’sob_typepointer with a fakePyTypeObject/PyBufferProcsvtable, redirectingPyBuffer_Release(bf_releasebuffer) tosystem@pltwhenImage.open(path).load()finishes. - Lossless
SOF3JPEGs inim.thumbnail()(CVE-2026-112403/GHSA-8g5g-8vmr-9782, reported by Anthropic and Ada Logics): Callingim.thumbnail((256, 256))invokesJpegImageFile.draft(), which requests1/2,1/4, or1/8scaling (cinfo.scale_denom = s) and shrinksstate->xsizeto (\lceil W / s \rceil). However,libjpeg-turbo3.x(bundled in all official Pillow wheels) supports 8-bit spatial lossless (SOF3, marker0xFFC3) JPEGs and silently ignoresscale_denomfor 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): Insrc/PIL/EpsImagePlugin.py,Ghostscript()placedinfile = fp.namedirectly after"-f"in thegsargument list. Even though Pillow passes-dSAFER, Ghostscript still parses any argument after-fthat starts with-as a flag and any argument starting with@as an@argfilecontaining 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 Ghostscript10.02.1through10.07.1. Commita2c363141da3(PR#10090) fixes this withinfile = os.path.abspath(fp.name). - Bundled
libopenjp22.5.4(GHSA-78g4-xqwv-q3g8): Every prebuiltPillowwheel from11.1.0through12.3.0shipspillow.libs/libopenjp2-*.so.2.5.4(or2.5.3). Inopj_j2k_read_sod(src/lib/openjp2/j2k.c), a JPEG 2000 codestream with a validTLMmarker andTNsot = 0in itsSOTmarkers uses the one-byteTPsotfield (l_current_tile_part) to indextp_index[]without a bounds check, walking up to 5,856 bytes past a 24-byte heap allocation and overwriting adjacentopj_event_mgr_tfunction pointers. Because OpenJPEG has not cut a release sincev2.5.4(September 2025), host OS patches do not fix virtualenv wheels; Pillow13.0.0(commit5f8114c8096b) backports upstream OpenJPEG commit91d08b11directly 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)
- Upgrade to
Pillow>=13.0.0:
Note thatpip install --upgrade "Pillow>=13.0.0"Pillow13.0.0drops Python3.10support (requires_python >= 3.11) and there is no12.xsecurity backport. Services still pinned to Python3.10must either upgrade the runtime to Python3.11+or apply the five C and Python patches above to a source build of12.3.0. - Restrict
Image.open()codecs withformats=[...]: BecauseImage.open()sniffs magic bytes rather than file extensions orContent-Typeheaders, 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() - 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 automaticFpxImagePluginandMicImagePlugindetection (CVE-2026-112398). - Remove the
ghostscriptpackage (apt-get purge ghostscript) from image-processing containers that do not render EPS files, and always passos.path.abspath(path)or aBytesIOstream toImage.open()(CVE-2026-112900). - Call
im.load()beforeim.thumbnail()(or passreducing_gap=None) to skipJpegImageFile.draft()(CVE-2026-112403). - Save compressed TIFFs to a temporary file (
tempfile.NamedTemporaryFile) rather thanio.BytesIO()solibtiffusesTIFFFdOpeninstead of_tiffWriteProc(CVE-2026-112393).
- Uninstall
Timeline
- 2026-02-10: OpenJPEG merges commit
91d08b11fixing theopj_j2k_read_sodheap overflow onmaster, without cutting a new release tag afterv2.5.4. - 2026-10-16:
Pillow13.0.0is 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, andCVE-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
- oss-security: Pillow 13.0.0 fixes long list of vulnerabilities
- Pillow 13.0.0 release notes
- Pillow advisory GHSA-v9pv-5gc8-w2m6 (CVE-2026-96639)
- Pillow advisory GHSA-5x8w-8jj5-q52h (CVE-2026-112398)
- Pillow advisory GHSA-8g5g-8vmr-9782 (CVE-2026-112403)
- Pillow advisory GHSA-332p-7hrc-92wf (CVE-2026-112393)
- Pillow advisory GHSA-w889-685w-c8fh (CVE-2026-112900)
- Pillow advisory GHSA-78g4-xqwv-q3g8 (bundled libopenjp2 2.5.4 heap overflow)
- OX Security: Three critical Pillow CVEs, RCE and heap overflows in Python's imaging library
- Debian Security Tracker: CVE-2026-112393