Skip to main content
VulnerabilityPublished

CVE-2026-96778: Pillow · Heap buffer overflow writing JPEG comment after ICC profile fills buffer

Affects aiderSource GCVE Database

0
Compare
Authoritative record:CVE-2026-96778· High 7.7

Not known to be exploited · CVSS 7.7 High.

Data as of 30 Oct 2026, 07:30 UTC

CVSS 4.0 base score
7.7High
How severe the flaw is on the 0–10 CVSS scale.

Thin evidence

One scorer or none
CVSS scores by source
CNA (GitHub_M)CVSS 4.07.7
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 21 Oct 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponized: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 29 Oct 2026.

What to do

Affected software

ProductAffectedFixed in
Homebrewaiderpkg:brew/aiderOther threatsAffected
  • >=0.86.1_1
Fixed in–

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

``` ============================================================================ Heap buffer overflow writing JPEG comment after ICC profile fills buffer Location: src/libImaging/JpegEncode.c:330 in ImagingJpegEncode() Commit: a707c6b21c313880d0771e54965d142dd42358b6 ============================================================================ ``` A heap buffer overflow in Pillow's JPEG encoder lets anyone who can supply an image write up to 64 KB of bytes they choose, followed by the rest of the compressed image, past the end of a 64 KB heap buffer. The target is any application that re-saves the image as JPEG while keeping its colour profile. The reliable result is that the Python process is killed outright rather than raising an exception. Because the overwritten memory sits directly in front of function pointers that the encoder calls next, this could allow code execution, though the attached reproducer demonstrates only the crash. The bug is in `ImagingJpegEncode()` in `src/libImaging/JpegEncode.c`. State 4 (line 330) calls `jpeg_write_marker()` for the COM comment marker without checking that the output buffer has room. Meanwhile, `JpegImagePlugin._save()` sizes that buffer from the EXIF and ICC/XMP data only and reserves nothing for the comment. When a 65498-byte ICC profile fills the 65536-byte buffer exactly, libjpeg's `emit_byte()` stores one byte past the end and then decrements `free_in_buffer` from 0. The `size_t` counter wraps, so the marker, the comment and the entire compressed image are written out of bounds. Triggering it takes only a valid JPEG (or PNG) carrying that profile and any comment, passed through `Image.open()` and then `im.save(..., "JPEG", icc_profile=im.info.get("icc_profile"))`. That is the documented way to preserve colour. The comment is forwarded from `im.info` automatically, and no non-default setting is involved. The attached reproducer re-saves such a file on the stock build, and the process dies with SIGSEGV. An identical file without the comment saves normally. An ASan build of Pillow will not flag this, because the stray writes happen inside the system libjpeg-turbo. Valgrind does report it, as an invalid write immediately after the 65569-byte allocation. The unguarded call was introduced in e50ae85ea406 ("Use jpeg_write_marker to write comment", 2022-12-05) and first shipped in 9.4.0. ## Details `ImagingJpegEncode()` (`src/libImaging/JpegEncode.c`) is a resumable state machine that fills a caller-supplied output buffer of `bytes` bytes and returns when it is full. State 4 writes the JPEG COM marker with `jpeg_write_marker()` without first checking that the buffer has any room left. When the preceding state 3 — the verbatim copy of the "extra" APP segments (ICC profile / XMP) — ends exactly at the last byte of the buffer, `jpeg_write_marker()` is entered with `free_in_buffer == 0`. libjpeg's `emit_byte()` stores the byte first and tests `--free_in_buffer == 0` afterwards, so the `size_t` counter wraps to 2^64-1 instead of triggering the "buffer full" path, and the 4-byte marker header, the whole comment, and then (state 5) the DQT/SOF/DHT/SOS headers and the entire entropy-coded image are written linearly past the end of the 65536-byte heap buffer allocated in `src/encode.c`. A JPEG laid out as ``` FFD8 SOI FFE2 <len> "ICC_PROFILE\0" 01 01 <65498 bytes> -> im.info["icc_profile"] FFFE <len> <comment, any length 1..65533> -> im.info["comment"] <any baseline RGB JPEG body> ``` opened with `Image.open()` and re-saved with the usual colour-profile-preserving call ```python im.save(out, "JPEG", icc_profile=im.info.get("icc_profile")) ``` kills the Python process on the stock build at this commit: glibc aborts with `realloc(): invalid next size` / `corrupted size vs. prev_size`, or the process takes SIGSEGV inside `jpeg_write_marker`/`jpeg_write_scanlines`. Stopping at `JpegEncode.c:330` in gdb shows `next_output_byte - buf == 65536`, `free_in_buffer == 0`, `comment_size == 65533` before the call, and `next_output_byte - buf == 131073`, `free_in_buffer == 18446744073709486079` after it — 65537 bytes already written beyond a 65536-byte buffer before a single scanline has been encoded. An ASan build of Pillow does not flag this, because the out-of-bounds stores happen inside the (uninstrumented) system libjpeg-turbo; valgrind does, reporting `Invalid write ... 0 bytes after a block of size 65,569` from `jpeg_write_marker` called at `JpegEncode.c:330` before aborting on the corrupted heap metadata. ### Code path The buffer. `JpegImagePlugin._save()` sizes the encoder buffer from the metadata it is about to emit, allowing one spare byte after the extra block and nothing for the comment: ```python # src/PIL/JpegImagePlugin.py:859-862 else: # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block.

References

Community reviews

Loading reviews…