GHSA-78g4-xqwv-q3g8: High severity pip/pillow vulnerability

Published Oct 22, 2026
·
Updated

Summary

Every Pillow wheel from 11.1.0 onward bundles a libopenjp2 containing a heap-buffer-overflow write that is fixed upstream on master but has never appeared in an OpenJPEG release. See https://osv.dev/vulnerability/OSV-2025-219

Because the wheel bundles its own copy, a patched host OS does not help. It is reachable from Image.open(fp); im.load() with default settings.

I am reporting it here rather than only upstream because Pillow is where the vulnerable binary actually reaches users.

Note the inversion that makes this easy to miss: OpenJPEG 2.5.2 and earlier are safe. The bug was introduced in 954c6e3c (2024-06-25), so keeping the dependency current is what creates the exposure.

Details

Affected wheels

From .github/dependencies.json (openjpeg) at each tag:

| Pillow | bundled OpenJPEG | status | |---|---|---| | 10.4.0 | 2.5.2 | safe | | 11.0.0 | 2.5.2 | safe | | 11.1.0, 11.2.1, 11.3.0 | 2.5.3 | affected | | 12.0.0 … 12.3.0 | 2.5.4 | affected |

Confirmed on an installed wheel: PIL.features.versioncodec('jpg2000') → 2.5.4, bundled file pillow.libs/libopenjp2-.so.2.5.4. main still pins 2.5.4 as of 2026-10-06.

The defect

src/lib/openjp2/j2k.c, in opjj2kreadsod:

c OPJUINT32 lcurrenttilepart = lcstrindex->tileindex[pj2k->mcurrenttilenumber].currenttpsno; lcstrindex->tileindex[...].tpindex[lcurrenttilepart].endheader = lcurrentpos; lcstrindex->tileindex[...].tpindex[lcurrenttilepart].endpos = lcurrentpos + pj2k->mspecificparam.mdecoder.msotlength + 2;

tpindex is neither null-checked nor bounds-checked, and lcurrenttilepart comes directly from the one-byte TPsot field of the SOT marker.

The correct guard already exists a few lines later in opjj2kaddtlmarker (j2k.c:8459), writing the same array at the same index behind if (tpindex && lcurrenttilepart < nbtps). One writer checked, its sibling not.

Reached by giving the codestream a valid TLM marker — so tpindex is allocated once by opjj2kbuildtpindexfromtlm, sized to the TLM entry count, and opjj2kreadsot then skips all resizing — while setting TNsot = 0 in every SOT, which keeps the TLM "valid" and lets TPsot walk past the allocation.

Patch status

Fixed upstream in 91d08b11 ("Fix heap buffer overlfow in opjj2kreadsod", 2026-02-10).

The newest OpenJPEG tag is v2.5.4 (2025-09-20), and compare/v2.5.4...91d08b11 reports ahead 6, behind 0 — the fix is in no release, 8 months on.

Suggested remediation

1. Carry 91d08b11 as a patch in the wheel build until an OpenJPEG release includes it — three lines, identical to a guard already present in the same file. 2. Consider asking OpenJPEG to cut a release; they have had an unreleased memory-corruption fix on master since February 2026, and every distro is shipping the vulnerable code. 3. Your security handbook already tells applications to block EPS at the application layer. A similarly explicit note that Image.open() ignores filename/Content-Type and will reach any compiled-in codec unless formats= is passed would help downstream apps a lot.

Not a Pillow bug, but relevant: the extension allowlist most apps rely on does not apply

Image.open() sniffs magic bytes, so a JPEG 2000 codestream in a file named avatar.png declared Content-Type: image/png reaches the JP2 decoder anyway.

I verified this against a production service documenting PNG/JPEG-only uploads: it accepted and fully decoded a JP2 through exactly that path, with 16 formats reachable from one upload endpoint against 43 registered openers. That service has since fixed it by passing formats= and validating on the detected format rather than the client's Content-Type.

That is the reason a decoder-level issue like this one has much wider reach than downstream maintainers tend to assume.

Proof of concept

ASan build of OpenJPEG 2.5.4 interposed under a stock Pillow 12.3.0, running Image.open(p); im.load():

==766332==ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 8 #0 opjj2kreadsod j2k.c:5086 #1 opjj2kreadtileheader j2k.c:9912 #2 opjreadtileheader openjpeg.c:575 #3 j2kdecodeentry src/libImaging/Jpeg2KDecode.c:793 #4 ImagingJpeg2KDecode src/libImaging/Jpeg2KDecode.c:949 #5 decode src/decode.c:132 located 8 bytes to the right of a 24-byte region allocated by opjj2kbuildtpindexfromtlm j2k.c:8939

Frames 3–5 are Pillow's own decoder, not a synthetic harness. No non-default decoder options are involved; strict mode is on, which is Pillow's behaviour since Pillow never calls opjdecodersetstrictmode. A benign twin image runs the same path cleanly and returns a valid image, so this is the payload and not a parse failure.

I have deliberately not attached the PoC file to this initial report. I'm happy to attach the PoC, the builder script and the RIP-control harness here on request, or hold everything until a fix is ready.

No disclosure deadline from me; I will not publish anything while this is being worked on.

Impact

Any application that calls Image.open() / im.load() on an image it did not produce — the overwhelmingly common case for upload handlers, thumbnailers and preview pipelines — can be made to perform an out-of-bounds heap write by supplying one crafted file.

Floor, already certain. On a stock non-sanitised wheel the overflow performs 245 writes walking to +5856 bytes past a 24-byte allocation, and glibc's own heap-metadata checker aborts the process (corrupted size vs. prevsize, corrupted double-linked list, SIGABRT). One file reliably kills a long-lived worker.

Ceiling, demonstrated as a primitive but not as a finished exploit. With same-size-class objects alive on both sides — an ordinary busy heap — the walk crosses opjeventmgrt, which holds three opjmsgcallback function pointers, and opjeventmsg then calls one (event.c:136):

rip 0x4000 <- value chosen by the attacker's file layout rsi 0x3ff2 <- merrordata, written by the same walk

Shifting the codestream with a COM marker moves rip with it (0x16ce → 0x4000), so the instruction pointer is attacker-chosen rather than incidentally invalid. A benign control image leaves the same groomed heap untouched.

To be explicit about what I did not do: I have not built a working exploit. That additionally needs remote heap grooming and an ASLR defeat, and I attempted neither. I am reporting a controlled control-flow primitive, not a finished chain. The CVSS AC:H reflects that.

Affected Software

1 affected componentFixes available
pip/pillow>=11.1.0<13.0.0
13.0.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/pillow to a version that resolves this vulnerability.

    Fixed in 13.0.0
  2. Upgrade

    Upgrade OpenJPEG to a version that resolves this vulnerability.

    Patch 91d08b11
  3. Configuration

    Pass the formats= argument to Image.open() and validate the detected format rather than trusting the filename or client-supplied Content-Type.

    Pillow Image.open() formats = Formats explicitly allowed by the application

Event History

Oct 22, 2026
Advisory Published
via GitHub·05:01 PM
Data Sourced
via GitHub·05:01 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which Pillow versions are affected, and which known versions are not?

Pillow 11.1.0, 11.2.1, and 11.3.0 bundle OpenJPEG 2.5.3 and are affected. Pillow 12.0.0 through 12.3.0 bundle OpenJPEG 2.5.4 and are affected, while Pillow 10.4.0 and 11.0.0 bundle OpenJPEG 2.5.2 and are identified as safe.

2

Does a patched operating-system OpenJPEG package remove the risk?

No. Pillow wheels bundle their own libopenjp2 binary, so updating the host operating system's OpenJPEG package does not remediate an affected Pillow wheel.

3

Is the vulnerable code reachable with Pillow's default settings?

Yes. The vulnerable code path is reachable with default settings when an application calls Image.open(fp) followed by im.load().

4

How can I verify whether an installed Pillow wheel contains the affected bundled codec?

Check PIL.features.version_codec('jpg_2000'). A result of 2.5.3 or 2.5.4 corresponds to the affected bundled OpenJPEG versions described here; an installed wheel may also contain a pillow.libs/libopenjp2-*.so file indicating the bundled library version.

5

What can be done if an affected wheel cannot be patched immediately?

Use a Pillow version known to bundle OpenJPEG 2.5.2, such as Pillow 10.4.0 or 11.0.0, rather than an affected wheel. This matters because OpenJPEG 2.5.2 and earlier are identified as safe for this issue.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203