GHSA-78G4-XQWV-Q3G8

GHSA-78G4-XQWV-Q3G8 is a high-severity out-of-bounds write vulnerability in pillow (pip), affecting versions >= 11.1.0, < 13.0.0. It is fixed in 13.0.0.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

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()

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:

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.

A write operation targets a memory location beyond the intended buffer boundary. Typical impact: memory corruption, crash, or arbitrary code execution.

GHSA-78G4-XQWV-Q3G8 has a CVSS score of 8.1 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (13.0.0); upgrading removes the vulnerable code path.

Affected versions

pillow (>= 11.1.0, < 13.0.0)

Security releases

pillow → 13.0.0 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade pillow to 13.0.0 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is GHSA-78G4-XQWV-Q3G8? GHSA-78G4-XQWV-Q3G8 is a high-severity out-of-bounds write vulnerability in pillow (pip), affecting versions >= 11.1.0, < 13.0.0. It is fixed in 13.0.0. A write operation targets a memory location beyond the intended buffer boundary.
  2. How severe is GHSA-78G4-XQWV-Q3G8? GHSA-78G4-XQWV-Q3G8 has a CVSS score of 8.1 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of pillow are affected by GHSA-78G4-XQWV-Q3G8? pillow (pip) versions >= 11.1.0, < 13.0.0 is affected.
  4. Is there a fix for GHSA-78G4-XQWV-Q3G8? Yes. GHSA-78G4-XQWV-Q3G8 is fixed in 13.0.0. Upgrade to this version or later.
  5. Is GHSA-78G4-XQWV-Q3G8 exploitable, and should I be worried? Whether GHSA-78G4-XQWV-Q3G8 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether GHSA-78G4-XQWV-Q3G8 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix GHSA-78G4-XQWV-Q3G8? Upgrade pillow to 13.0.0 or later.

Other vulnerabilities in pillow

Stop the waste.
Protect your environment with Kodem.