Heap overflow in Pillow's bundled OpenJPEG JPEG 2000 decoder
Key facts
- Attack vector
- —
- Privileges required
- —
- User interaction
- —
- Attack complexity
- —
- Impact type
- —
- Confidentiality · integrity · availability
- —
- Fix status
- Fixed
No CVSS vector from any source. Fix status from GitHub.
Summary
Pillow from 11.1.0 before 13.0.0 bundles a libopenjp2 library with a heap buffer overflow in opj_j2k_read_sod, where a tile part index taken from the codestream is used without a bounds check, so the write lands out of bounds (CWE-787). An attacker can trigger it with a crafted JPEG 2000 file that Pillow opens and loads with default settings. Because each Pillow wheel carries its own copy of the library, updating the host operating system does not remove the flaw.
Written with Claude Haiku 5.5 from the sources below · 2026-10-28
Description from the GitHub advisory
### 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.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`: ```c 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.
Why it matters now
SSVC decision points
Not available: CISA has not published SSVC decision points for this record.
Affected products
| Vendor | Product | Affected versions | Fixed in | Status | Source |
|---|---|---|---|---|---|
| — | pillow (PyPI) | 11.1.0 to before 13.0.0 | 13.0.0 | Patch available | GHSA-78g4-xqwv-q3g8 |
- pillow (PyPI) Vendor— Affected 11.1.0 to before 13.0.0 Fixed in 13.0.0 Patch available GHSA-78g4-xqwv-q3g8
Remediation
Upgrade the pillow PyPI package to version 13.0.0 or later. The fix exists in OpenJPEG as commit 91d08b11 on master, and no OpenJPEG release includes it.
Written with Claude Haiku 5.5 from the sources below · 2026-10-28
- Fix · GHSA-78g4-xqwv-q3g8 pillow (PyPI) Fixed in 13.0.0 Advisory →
Timeline
- 2026-10-22 Threatist record created from GHSA-78g4-xqwv-q3g8
References
Other 7
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-78g4-xqwv-q3g8
- https://github.com/python-pillow/Pillow/commit/5f8114c8096b181fae493543cd3837d9f9a971cc
- https://github.com/python-pillow/Pillow/commit/d3470c327840f4c216a1610f30e7091dfd0b97c0
- https://github.com/uclouvain/openjpeg/commit/91d08b11a72764f6d199f32fa0c1b1abd4edc2ad
- https://github.com/python-pillow/Pillow
- https://github.com/python-pillow/Pillow/releases/tag/13.0.0
- https://osv.dev/vulnerability/OSV-2025-219
Sources
This product uses the NVD API but is not endorsed or certified by the NVD. Sources and licences