Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

Pillow: Bundled libopenjp2 2.5.4 in all wheels since 11.1.0 carries OSV-2025-219 heap-buffer-overflow write reachable from Image.open().load()GHSA-78g4-xqwv-q3g8

HIGHFix: python-pillow/Pillow@5f8114c

GHSA-78g4-xqwv-q3g8 is a high-severity (CVSS 8.1) remote code execution vulnerability in pillow. A fix is available for pillow — see the affected versions and patch details below.

Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 22, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐍pillow

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

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:

Pillowbundled OpenJPEGstatus
10.4.02.5.2safe
11.0.02.5.2safe
11.1.0, 11.2.1, 11.3.02.5.3affected
12.0.0 … 12.3.02.5.4affected

Confirmed on an installed wheel: PIL.features.version_codec('jpg_2000') → 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 opj_j2k_read_sod:

OPJ_UINT32 l_current_tile_part =
    l_cstr_index->tile_index[p_j2k->m_current_tile_number].current_tpsno;
l_cstr_index->tile_index[...].tp_index[l_current_tile_part].end_header = l_current_pos;
l_cstr_index->tile_index[...].tp_index[l_current_tile_part].end_pos   =
    l_current_pos + p_j2k->m_specific_param.m_decoder.m_sot_length + 2;

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

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

Reached by giving the codestream a valid TLM marker — so tp_index is allocated once by opj_j2k_build_tp_index_from_tlm, sized to the TLM entry count, and opj_j2k_read_sot 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 opj_j2k_read_sod", 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 opj_j2k_read_sod          j2k.c:5086
    #1 opj_j2k_read_tile_header  j2k.c:9912
    #2 opj_read_tile_header      openjpeg.c:575
    #3 j2k_decode_entry          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
    opj_j2k_build_tp_index_from_tlm 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 opj_decoder_set_strict_mode. 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. prev_size, 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 opj_event_mgr_t, which holds three opj_msg_callback function pointers, and opj_event_msg then calls one (event.c:136):

rip  0x4000    <- value chosen by the attacker's file layout
rsi  0x3ff2    <- m_error_data, 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 Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIpillow≥ 11.1.0&&< 13.0.013.0.0pip install --upgrade 'pillow==13.0.0'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pillow, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update pillow to 13.0.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-78g4-xqwv-q3g8 is resolved across your whole dependency graph.

  3. Workarounds

    Stop feeding it untrusted input: reject or quarantine files and payloads from unverified sources until you can upgrade, restrict accepted formats to the ones you actually need, and run the parsing or decoding step in a least-privileged sandbox or short-lived worker so a crash or corrupted read cannot reach the rest of the process.

Frequently Asked Questions

### 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.**
O3 Security · Impact-Aware SCA

Is GHSA-78g4-xqwv-q3g8 in your dependencies?

Find it across PyPI, including transitive dependencies.

Pillow: Bundled libopenjp2 2.5.4 in all wheels since 11.1.0…