SevyDevy

Guide · Backend

Pillow 13 Security Update Explained (October 2026): What Image Uploads Need Now

20 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The Pillow 13 security update is Pillow 13.0.0, released on 16 October 2026. Pillow is the Python library behind "from PIL import Image", and its release notes list 38 security fixes.
  • If your app opens images that strangers upload, you are exposed on 12.3.0 and earlier. Both critical entries are memory bugs where Pillow writes past the memory block it reserved, which can let a crafted file run code.
  • As of 25 October 2026 there is no 12.x fix on PyPI. Pillow 13 needs Python 3.11 or newer, so on Python 3.10 a plain upgrade quietly stays on the vulnerable 12.3.0.
  • Three habits reduce your exposure: Pillow's pixel limit (Image.MAX_IMAGE_PIXELS), an allowlist (the only formats you accept) in Image.open(), and an early load() call, which decodes the file into pixels. None of them replaces the upgrade.
  • Beginners can stop after the section on what to do today. Engineers then get tested surprises: a check that passes a broken JPEG, a JPEG allowlist that also admits multi-picture files, and an upload helper run on both versions.

What goes wrong when your app opens an image someone uploaded?

The file can attack the code that reads it. An image is a set of instructions for rebuilding pixels, and a crafted set can push the reader into a bug. The Pillow 13 security update, released as Pillow 13.0.0 on 16 October 2026, fixes 38 such problems, many of them in the code that reads image files.

Picture a common setup. A small online marketplace lets sellers upload product photos, and a Python backend makes thumbnails with Pillow. The setup is made up, but the bugs and versions below are real. Nobody on the team ever writes code that looks dangerous.

The seller's file arrives with any name, such as shoe.jpg. Pillow does not rely on the name. It reads the first bytes, guesses the format, and hands the file to that format's reader. A crafted file can steer Pillow into a reader you never meant to use.

Three kinds of failure follow. The worker can run out of memory and crash, which is a denial of service. It can spin at full CPU, in one reported case without end. In the worst case, a memory bug lets the attacker run their own commands on your server.

Which words do you need before reading the advisories?

Five terms carry most of the advisories. Each one is explained here once and used the same way after.

  • Decoder: the code that turns a file's compressed bytes back into pixels. Pillow has one per format, and most bugs live here.
  • Heap overflow: the decoder writes past the end of the memory block it reserved. Nearby data gets overwritten, which can crash the process or hand control to an attacker.
  • Decompression bomb: a small file that claims a huge picture. A few kilobytes can ask for gigabytes of memory once decoded.
  • Frame and seek(): some formats hold several pictures, called frames. Calling seek(n) moves to frame n, and Pillow reads that frame's header again.
  • olefile: an optional Python package that reads Microsoft's old compound file format. Pillow needs it for FlashPix (FPX) and MIC images.

What is the Pillow 13 security update?

It is Pillow 13.0.0, a major release that ships security fixes together with breaking changes. Its release notes list 2 critical, 13 high, 20 medium and 3 low entries. One critical entry covers two separate reports, so the table below shows three critical rows. The fixes landed in 13.0.0 only.

A major release, in Pillow's versioning policy, is one that can remove old features and drop Python versions that reached end of life. Pillow does this every October. This year it dropped Python 3.10 and removed several deprecated features.

Each advisory has a GHSA ID, which is GitHub's security advisory number. Many also have a CVE ID, the industry-wide number that scanners use. We matched each pair below against the CVE record or the OSV database before printing it.

The advisories that matter most for upload handling (checked 25 October 2026)
AdvisorySeverityWhat goes wrongWho is exposed
GHSA-v9pv-5gc8-w2m6, CVE-2026-96639CriticalMulti-frame readers reuse old pixel memory when a later frame needs more bytes per pixelPillow 11.0.0 to 12.3.0; PSD, MPO, DCX and MIC files
GHSA-5x8w-8jj5-q52h, CVE-2026-112398CriticalFlashPix JPEG tiles write past their buffer; the advisory reports code execution12.3.0 and earlier, only when the olefile package is installed
GHSA-8g5g-8vmr-9782, CVE-2026-112403CriticalLossless JPEG shrunk by thumbnail() writes a full row into a smaller buffer12.3.0 and earlier with libjpeg-turbo 3, which official wheels bundle
GHSA-p8hx-v2p6-rp84, CVE-2026-96636MediumPixel limit skipped after seek() to a later frameMPO and DCX in the advisory; the fix also covers MIC
GHSA-g842-9rc4-69pw, CVE-2026-96637HighSame skip in SPIDER files, a scientific image format12.3.0 and earlier

The release notes group the FlashPix report and the lossless JPEG report into one critical entry. That fits the code, because one new size check in Pillow's JPEG decoder closes both.

Which Pillow 13.0.0 bugs matter most for image uploads?

The ones a stranger can trigger with a single uploaded file. Three groups stand out: memory-safety bugs in decoders, ways around the pixel limit, and the FlashPix bug that depends on olefile.

Memory bugs in multi-frame readers

The top critical advisory, CVE-2026-96639, affects readers that hold several frames. Each frame has a pixel mode, which sets how much memory each pixel needs. On 12.3.0 a reader could keep the memory sized for an earlier frame. If a later frame used a wider mode, such as RGB after grayscale, Pillow wrote it into the smaller block.

The fix in 13.0.0 is short. When a later frame has a different size or mode, Pillow now throws away the stored pixels instead of reusing them. The MPO, DCX, MIC and PSD readers all use this new rule.

The pixel limit only guarded the first frame

Pillow checks an image's width times height when Image.open() reads the header. On 12.3.0, the MPO, DCX, MIC and SPIDER readers re-read the header on seek() and never repeated that check. Pillow 13.0.0 adds the check to each of those seek() methods.

The advisories give the scale. An MPO file of about 3 MB reached about 763 MB of peak memory. In the SPIDER advisory's worked example, a file under 1 KB asks for about 6.4 GB, because the format stores 4 bytes per pixel.

Think of a courier depot guard who checks the label on the first box of every shipment. The label is the image header. The guard is Pillow's pixel check. The other boxes in the shipment are the later frames, and on 12.3.0 nobody read their labels.

Where the picture breaks: a real guard can open a box, but Pillow cannot know the true decoded size without decoding. It can only trust the header, so a check that runs at the right moment is all it has.

Where the pixel check runs for a multi-frame file

Notice the branch labelled 12.3.0 or older. Frame 1's header was trusted without a size check, so memory was reserved before anyone counted pixels.

Diagram connections:

  • Uploaded MPO or DCX file to Image.open()
  • Image.open() to Pixel check on frame 0
  • Pixel check on frame 0 to Your code calls seek(1)
  • Your code calls seek(1) to Which Pillow version?
  • Which Pillow version? to load() reserves memory (12.3.0 or older)
  • Which Pillow version? to DecompressionBombError (13.0.0)
  • Uploaded MPO or DCX file: Frame 0 tiny, frame 1 huge
  • Image.open(): Reads frame 0 header only
  • Pixel check on frame 0: Passes: frame 0 is small
  • Your code calls seek(1): Reader parses frame 1 header
  • Which Pillow version?: The fix lives inside seek()
  • load() reserves memory: No check: memory spike or crash
  • DecompressionBombError: Check runs again before any decode

FlashPix needs olefile, which you may not know you have

The FlashPix bug, CVE-2026-112398, is reachable only when olefile is installed. Pillow then recognises FlashPix by its first bytes, so a file called photo.jpg can still reach the FlashPix reader. The advisory reports code execution.

A plain pip install pillow does not install olefile. You get it through the pillow[fpx] or pillow[mic] extras, or through another package that depends on it. We confirmed this on 13.0.0: FPX and MIC appear in Pillow's format list only when olefile imports.

Is there a Pillow 12.x fix, or must you upgrade to 13?

You must upgrade to 13.0.0. As of 25 October 2026, every advisory we opened lists 13.0.0 as the only fixed version, and PyPI has no release after 12.3.0 on the 12 line.

Pillow's versioning policy does allow patch releases for security fixes. Nothing on PyPI shows one for these advisories. Recheck PyPI before you plan around a 12.3.1, because that could change.

The bigger trap is Python 3.10. Pillow 13.0.0 declares Requires-Python 3.11 or newer. On Python 3.10, pip simply picks the newest release that fits, which is the vulnerable 12.3.0, and prints no warning about it.

We checked this with pip download on a Python 3.10 target. Asking for pillow gave pillow-12.3.0. Asking for pillow==13.0.0 failed with "No matching distribution found for pillow==13.0.0". On a Python 3.11 target, the same command gave 13.0.0.

If you install Pillow from your Linux distribution instead of PyPI, the distribution decides when you get fixes. On 25 October 2026, Debian's tracker for CVE-2026-112398 showed only the unstable branch fixed, through 13.0.0.

What should you do today to protect image uploads?

Upgrade every environment that opens untrusted images to Pillow 13.0.0, after moving it to Python 3.11 or newer. Then add the three defences below, which help against the next batch of bugs too.

  1. Find every copy of Pillow

    Check web servers, background workers, notebooks and container images. Run python -c "import PIL; print(PIL.__version__)" in each one.

  2. Check the Python version

    Run python --version. If it prints 3.10 or older, upgrade Python first. Otherwise pip keeps installing 12.3.0 on Python 3.10, or 11.3.0 on 3.9, and both are vulnerable.

  3. Run your tests on 12.3.0 with warnings as errors

    Use python -W error::DeprecationWarning -m pytest. Four of the six removed features we tested raise a warning on 12.3.0, so this finds most breakage early.

  4. Pin and install 13.0.0

    Set pillow==13.0.0 in your requirements file and reinstall. Rebuild container images, because an old layer can keep the old wheel.

  5. Remove olefile if you do not need it

    Run pip show olefile and read its Required-by line. If nothing needs it and no code of yours reads FlashPix or MIC files, uninstall it.

  6. Harden the upload path

    Allow only the formats you need, reject images over a pixel budget, decode with load() inside a try block, and reject multi-frame files you do not need.

How do you upgrade from Pillow 12 to 13 without breaking code?

Most code needs no change at all. The release removes several deprecated features and changes three behaviours. Search your code for each one before you deploy.

What 13.0.0 removed, tested on 12.3.0 and 13.0.0 with Python 3.13
Removed or changedWarns on 12.3.0?What to use on 13.0.0
Saving an I mode (32-bit integer) image as PNGYes, DeprecationWarningConvert first, for example im.convert("I;16"); 13.0.0 raises OSError otherwise
Image.Image.get_child_images()YesCall it on the opened file object, an ImageFile.ImageFile, which still has it
ImageCms.ImageCmsProfile.product_name and product_infoYesNothing; they have been None since Pillow 2.3.0
Image.fromarray() with a mode that changes the data typeYesLeave mode out, or convert the array to the right type first
ExifTags.IFD.MakernoteNo warning on access in our testExifTags.IFD.MakerNote
Image._showNo warning on access in our testPIL.ImageShow.show()
Python 3.10Not applicablePython 3.11 or newer

Three API changes do not raise errors, so tests may miss them. ImageColor.getcolor() now returns premultiplied values for RGBa and La modes. Duplicate TIFF tags now keep the first value. Saving a tiny ICO without sizes now uses the image's own size.

How does Image.MAX_IMAGE_PIXELS protect you from decompression bombs?

It caps width times height before any pixels are decoded. On both 12.3.0 and 13.0.0 the default is 89,478,485 pixels, about 8.9 crore (89 million). Above that, Pillow warns. Above twice that, 178,956,970 pixels, it raises Image.DecompressionBombError.

The gap between the two numbers surprises people. A warning does not stop anything. In our test, a 15000 by 10000 image, 15 crore pixels, opened on default settings with only a warning.

We tested the rule with the limit lowered to 4,000 pixels. A 60 by 60 image opened quietly. A 70 by 70 image, 4,900 pixels, opened with a DecompressionBombWarning. A 100 by 100 image raised DecompressionBombError, because 10,000 is above 8,000.

You can turn the warning into an error with warnings.simplefilter("error", Image.DecompressionBombWarning). That filter is process-wide. In a web server, a plain size check in your own code is easier to reason about, and the helper below does that.

Setting Image.MAX_IMAGE_PIXELS = None turns the check off completely. Some tutorials suggest it to silence the warning. Never do that in a process that opens uploads.

Why is verify() not enough, and when should you call load()?

Because verify() does not decode pixels, it cannot catch a broken or malicious pixel stream. Its docstring in 13.0.0 says it checks the file "without actually decoding the image data". In the 13.0.0 source, only the PNG reader adds real checks to it.

We cut a JPEG of about 40,000 bytes to 60% of its length and tested both calls. verify() returned with no exception. load() raised OSError: image file is truncated. A truncated PNG did fail verify(), which matches PNG having its own checks.

There is a second trap. After verify(), the same image object cannot be loaded. On both versions, calling load() after verify() raised an AssertionError, because verify() closes the file. The docstring says to reopen the file first.

So the useful pattern is simple. Open the file, check its size and format from the header, then call load() inside a try block. Any decoding error then happens in one place you control.

load() does not make a buggy decoder safe. It runs the decoder, which is exactly where the memory bugs live. Its value is that failures happen early, inside your error handling, rather than later inside a thumbnail call.

Early load() also matters for one advisory. The lossless JPEG bug, CVE-2026-112403, runs through the shrink-while-decoding shortcut that thumbnail() uses. Its advisory lists load() before thumbnail() as a workaround, which is a stopgap, not a reason to stay on 12.3.0.

How do you limit formats with Image.open(formats=...)?

Pass a list or tuple of format names, and Pillow tries only those readers. With formats=["JPEG", "PNG", "WEBP"], a GIF raised UnidentifiedImageError on both versions. Passing a plain string such as "PNG" raises TypeError: formats must be a list or tuple.

This blocks a whole class of bugs. FlashPix, SPIDER, DCX, PSD and TIFF readers are never reached, whatever the file is called. Many of the 38 entries in the release notes sit in TIFF, PDF, EPS, GIF, BMP and other readers that a photo upload never needs.

The MPO surprise

Allowing JPEG also allows MPO. MPO, short for Multi Picture Object, stores several JPEG pictures in one file. Pillow's JPEG reader hands such files to the MPO reader.

We saved a two-frame MPO and opened it with formats=["JPEG"]. Pillow returned an MpoImageFile with format MPO and n_frames 2, on both versions. MPO is one of the four formats named in the critical CVE-2026-96639.

This behaviour is undocumented as of 25 October 2026; pin your version and recheck it after upgrades. Read im.format after opening, and reject files with more than one frame unless you need animation.

What does a hardened upload handler look like?

For the marketplace, it combines the three defences: a format allowlist, a pixel budget checked from the header, and an early load() inside one error handler. It also refuses multi-frame files, so your code never calls seek() on an upload.

python
import io

from PIL import Image

ALLOWED = ("JPEG", "PNG", "WEBP")
MAX_PIXELS = 40_000_000  # 4 crore pixels; pick a limit for your app


class BadImage(Exception):
    pass


def read_upload(data: bytes) -> Image.Image:
    try:
        with Image.open(io.BytesIO(data), formats=ALLOWED) as im:
            if im.width * im.height > MAX_PIXELS:
                raise BadImage(f"{im.width}x{im.height} is too many pixels")
            if getattr(im, "n_frames", 1) > 1:
                raise BadImage(f"{im.format} with {im.n_frames} frames")
            im.load()  # decode every pixel now, inside this try
            return im.copy()
    except Image.DecompressionBombError:
        raise BadImage("decompression bomb") from None
    except (OSError, SyntaxError, ValueError) as e:
        raise BadImage(type(e).__name__) from None

We ran this helper on 12.3.0 and 13.0.0 with eight test files, and both versions gave the same results. The table shows what each file did.

Output of read_upload() on Pillow 13.0.0 (12.3.0 gave the same results)
Test fileResultWhich check stopped it
800 by 600 PNGOK, (800, 600) RGBNone; accepted
256 by 256 JPEGOK, (256, 256) RGBNone; accepted
GIFBAD, UnidentifiedImageErrorformats= allowlist
Two-frame MPOBAD, MPO with 2 framesn_frames check
JPEG cut to 60%BAD, OSErrorload() inside the try block
8000 by 6000 PNG (4.8 crore pixels)BAD, too many pixelsYour own MAX_PIXELS check
15000 by 12000 PNG (18 crore pixels)BAD, decompression bombPillow's own DecompressionBombError
A text fileBAD, UnidentifiedImageErrorNo reader accepted it

What just happened? Every bad file failed with one exception type, BadImage, so the web layer can return a clean error. The same results on 12.3.0 show the helper works on either version. It still cannot fix a decoder bug inside load(), so the upgrade stays first.

For more safety, run image work in a separate worker process with a memory cap and a timeout. Then a crash or memory spike kills one worker, not the web server. Re-encode the accepted image as a fresh JPEG or PNG before you store it.

A safer upload path around Pillow

Notice that Pillow never runs in the web process. The worker is the only part that touches untrusted bytes, and it can die without taking the site down.

Diagram connections:

  • Seller's browser to Web server (Upload)
  • Web server to Raw upload bucket (Save as is)
  • Web server to Job queue (Enqueue job)
  • Job queue to Image worker (Pick up job)
  • Raw upload bucket to Image worker (Read bytes)
  • Image worker to Clean image bucket (Accepted)
  • Image worker to Rejection log (Rejected)
  • Seller's browser: Sends the photo
  • Web server: Checks byte size, stores raw file
  • Raw upload bucket: Untrusted bytes, never served
  • Job queue: One job per upload
  • Image worker: Pillow 13.0.0, memory cap, timeout
  • Clean image bucket: Re-encoded JPEG or PNG only
  • Rejection log: BadImage reasons for review

How do you check that your servers are really on Pillow 13?

Ask the running environment, not the requirements file. Three commands cover it, and each has a clear expected output.

bash
python -c "import PIL; print(PIL.__version__)"
# expected: 13.0.0

python -c "from PIL import Image; Image.init(); print('FPX' in Image.ID)"
# expected: False when olefile is not installed

pip install pip-audit
pip-audit -r requirements.txt --no-deps --disable-pip
# expected: no pillow lines in the output once pillow==13.0.0 is pinned

The audit tool, pip-audit, looks up each pinned package in a public vulnerability database. With pillow==12.3.0 it printed "Found 37 known vulnerabilities in 1 package" on 25 October 2026, each with fix version 13.0.0. That count can change as more CVEs are assigned.

Run the version command inside each container and worker, not only on your laptop. A worker image built last month can still carry 12.3.0 after the web server moved on.

How do you explain the Pillow 13 security update in a system design interview?

Use it as a case study in handling untrusted files. Interviewers care less about the CVE numbers and more about how your design limits damage when a library bug appears.

  1. Name the threat

    Uploaded images are attacker-controlled input. Decoders are complex native code, so they get memory bugs and decompression bombs.

  2. Patch fast, with a plan for breaking changes

    The fix shipped only in a major version that dropped Python 3.10. Say you keep runtimes current so a security upgrade is not blocked.

  3. Reduce what the decoder sees

    Allow a few formats, cap pixels from the header, reject extra frames and drop optional readers like FlashPix.

  4. Contain the blast radius

    Decode in an isolated worker with memory and time limits. Keep raw uploads in separate storage and serve only re-encoded copies.

  5. Detect and verify

    Run a dependency audit in CI, log rejected files, and check the running version in every service that decodes images.

Questions people ask

Is my app affected if it only resizes images it creates itself?
Much less. These bugs need a crafted file, so images your own code draws are low risk. The danger is any path where outside bytes reach Image.open(), including uploads, URLs you fetch, email attachments and files from partners. If any such path exists, upgrade.
Can I stay on Pillow 12.3.0 if I set Image.open formats to JPEG and PNG?
It helps, but it is not a fix. The allowlist keeps FlashPix, SPIDER and DCX away, yet JPEG still admits MPO files, which the critical CVE-2026-96639 names. The JPEG decoder overflow is also inside the JPEG reader itself. Upgrade to 13.0.0 and keep the allowlist as an extra layer.
Does uninstalling olefile fix the FlashPix vulnerability?
It removes the path to it, according to the advisory, which says the FlashPix reader is reached only when olefile is installed. We confirmed that FPX and MIC are not registered without olefile. It does nothing for the other bugs, so treat it as an extra step after the upgrade.
Why did pip install --upgrade pillow leave me on 12.3.0?
Your Python is probably 3.10. Pillow 13.0.0 requires Python 3.11 or newer, so pip quietly chooses the newest release your Python allows, which is 12.3.0. On 3.9 it picks 11.3.0, which is also vulnerable. Check with python --version, move to a supported Python, then reinstall pillow==13.0.0.
What is the difference between DecompressionBombWarning and DecompressionBombError?
The warning only reports; the error stops the open. Pillow warns when width times height is above Image.MAX_IMAGE_PIXELS, which is 89,478,485 by default. It raises the error above twice that number. Treat the warning as a failure in code that handles uploads.
Will Pillow 13 break my existing code?
Usually not. The breaking changes are narrow: Python 3.10 support, saving 32-bit I mode images as PNG and get_child_images() on plain images. Also removed are two ImageCms attributes, ExifTags.IFD.Makernote, Image._show and fromarray() mode changes that alter the data type. Run your tests on 12.3.0 with DeprecationWarning as an error to catch most of them first.

Sources and further reading

Practise this with Lala

Practise