CVE-2026-128067: Pillow · Heap buffer overflow saving TIFF with libtiff when tags override image size
Affects PillowSource GCVE Database
Not known to be exploited · CVSS 8.4 High : upgrade to 13.0.0 soon.
Data as of 13 Nov 2026, 00:06 UTC
Thin evidence
One scorer or none| CNA (GitHub_M) | CVSS 4.0 | 8.4 |
|---|
- Patch
- Available – fixed in 13.0.0
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:H/SC:N/SI:N/SA:NExploitation evidence
Disclosed: evidence found.
CVE published 12 Nov 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponized: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by 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 12 Nov 2026.
What to do
Upgrade Pillow to 13.0.0.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
| PyPIPillow | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
``` ============================================================================ ANT-2026-CS7N494P [high] CWE-787 memory-safety Heap buffer overflow saving TIFF with libtiff when tags override image size Location: src/libImaging/TiffDecode.c:1035 in ImagingLibTiffEncode() Commit: a707c6b21c313880d0771e54965d142dd42358b6 ============================================================================ ``` When Pillow saves an image as a compressed TIFF, size information taken from the image's metadata can make the encoder read past the end of a heap buffer. Someone who uploads an image can use this to get the server process's memory, including pointers, copied into the saved file, or to crash the process. When the metadata also enables TIFF "Predictor=2" compression, the encoder writes past the buffer instead, corrupting the heap and aborting the process. This affects applications that write TIFF through libtiff (any `compression=` other than raw) and either carry the source metadata across with `exif=im.getexif()` / `tiffinfo=`, or simply re-save a crafted palette TIFF with `Image.open(f).save(out, "TIFF")`, which inherits its compression and predictor from the upload. In `TiffImagePlugin._save`'s libtiff branch, only `BitsPerSample` is pinned to the real image when the tag list for the encoder is built (`src/PIL/TiffImagePlugin.py:1955`). `ImageWidth`, `ImageLength` and `PlanarConfiguration` are set from `im.size` but are then overwritten by the `exif=`/`tiffinfo=` merge loop at `:1768-1772`. `SamplesPerPixel` is never set for single-band modes, so a value from the caller's tags or from the source TIFF's own IFD reaches `TIFFSetField`. `ImagingLibTiffEncode` then passes a row buffer sized from the actual image (`src/encode.c:291`) to `TIFFWriteScanline` (`src/libImaging/TiffDecode.c:1035`) with no check against `TIFFScanlineSize()`, and libtiff consumes a full scanline of the tag-described width from it. The attached reproducer demonstrates the read case. A 64x4 greyscale JPEG whose EXIF says `SamplesPerPixel=3, Photometric=RGB` is converted into a valid RGB TIFF in which 128 bytes of each row are live heap, including pointers that change from run to run. The same JPEG with EXIF `ImageWidth=2**27` kills the process with SIGSEGV. For the write case, the values written are byte-wise differences of existing heap contents rather than attacker-chosen data, and nothing beyond the observed glibc abort is shown. The `tiffinfo=` route dates back to Pillow 2.3.0 (`dbf47837ce99`), and the `exif=` route to 8.4.0 (`c0f619384cbc`). ## Details When an image is saved as TIFF through the libtiff encoder (any `compression=` other than raw, or `TiffImagePlugin.WRITE_LIBTIFF`), libtiff sizes its scanline from the `ImageWidth` / `SamplesPerPixel` / `BitsPerSample` / `PlanarConfiguration` tags it was handed, Pillow sizes the encoder row buffer from the real image, and nothing compares the two. In `TiffImagePlugin._save`'s libtiff branch only `BitsPerSample` is pinned in the attribute dict given to the encoder (`atts`, `src/PIL/TiffImagePlugin.py:1955-1982`); the other geometry tags can come from caller- or attacker-supplied data: - `ImageWidth` (256), `ImageLength` (257) and `PlanarConfiguration` (284) are set from `im.size` *before* the `tiffinfo=` / `exif=` merge loop at `:1768-1772`, so they are overwritten by it — by the EXIF of an uploaded JPEG, or by stale geometry in `im.getexif()` / `im.tag_v2` of a TIFF that Pillow auto-transposed on load (Orientation tag) or that the application `thumbnail()`ed. - `SamplesPerPixel` (277) is only put in the IFD for multi-band modes (`:1847-1850`), so for single-band images (`L`, `P`, `I;16`, `1`, …) a value from `exif=` / `tiffinfo=`, or from the *source TIFF's own IFD* via `supplied_tags` (`im.tag_v2`, `:1782`), is forwarded to libtiff. A palette TIFF with `SamplesPerPixel=2, BitsPerSample=(8,8), ExtraSamples=(0,)` opens as mode `P` (rawmode `PX`, extra sample dropped), and a bare `im.save(out, "TIFF")` — no save arguments; `compression` and `Predictor` are inherited from the source — already makes libtiff process `2*width` bytes per row from a `width`-byte buffer. `ImagingLibTiffEncode` then hands the image-sized row buffer to `TIFFWriteScanline`, and libtiff reads one scanline of the *tag-described* geometry out of a buffer holding one row of the *actual* image. With `Predictor = 2` (also forwarded from `tiffinfo=` / `im.tag_v2`) libtiff's horizontal differencing first rewrites the whole scanline **in place**, so the over-read becomes an out-of-bounds heap **write** on every row. The tags do not have to be supplied deliberately by the application: `im.getexif()` of an uploaded JPEG returns whatever IFD0 tags the file carries, and the common metadata-preserving conversion ```python im = Image.open(uploaded_jpeg) # 64x4, mode "L" im.save(out, "TIFF", exif=im.getexif(), compression="tiff_lzw") ``` behaves as follows on the stock build at thi
References
- github.com/python-pillow/Pillow/security/advisories/GHSA-xcvg-w56q-r89m (opens in a new tab)
- github.com/python-pillow/Pillow/commit/53318b50e8e9e85a57d033d4d879e1398d97aeb1 (opens in a new tab)
- github.com/python-pillow/Pillow/commit/ec5f1cbcf67275641abaad5c065ffb64a0a804a9 (opens in a new tab)
- github.com/python-pillow/Pillow (opens in a new tab)
- github.com/python-pillow/Pillow/releases/tag/13.0.0 (opens in a new tab)