Pillow: TIFF encoder ColorMap plane-size mismatch causes a heap out-of-bounds read
CVE-2026-112896 · GHSA-pchr-8j38-3x4p · published 20 Oct 2026
How serious is it
- Severity: high, CVSS score 8.8 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-112896?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
Pillow(PyPI) <= 12.3.0
How to check
- Run
pip show Pillowin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
Pillowto 13.0.0 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
A small attacker-supplied TIFF can be opened and loaded normally, then reach an out-of-bounds read when an application saves it back to TIFF with retained metadata. The fixed reproducer is a 1×1, 16-bit grayscale, PackBits-compressed TIFF containing tag 320 (ColorMap) with **768 SHORT values**: three zero-filled planes of 256 entries each. No Python object is forged and no metadata is injected after loading.
Pillow's TIFF encoder allocates **768 × 2 = 1536 bytes**, then passes three plane pointers separated by 256 SHORT entries to libtiff. With `BitsPerSample = 16`, libtiff instead expects **65536 SHORT values per plane** and attempts a **131072-byte read** from a plane pointer. Existing ASAN observations identify a heap-buffer-overflow READ crossing the 1536-byte allocation made in `PyImaging_LibTiffEncoderNew`.
The malformed ColorMap need not be a valid palette for this grayscale image. The issue is that accepted metadata is forwarded on save without reconciling the fixed plane extent with the effective sample depth. Opening/loading alone is not the demonstrated fault; the encoder path is required.
## Impact and prerequisites
An application must accept the input, retain the relevant TIFF metadata, and invoke the libtiff-backed TIFF save path. The observed consequence is an out-of-bounds read and ASAN termination. Process-memory exposure during encoding is a potential consequence, **not demonstrated by extracting arbitrary heap contents in this report**. No arbitrary read primitive, full memory-disclosure chain, code execution, address-space bypass, or cross-process effect is claimed.
The 131072-byte attempted copy is not the number of bytes proven disclosed. This report does not run a synthetic-canary serializer or collect an unsanitized encoded output.
## Tested environment and limits of the evidence
The existing observations were recorded on August 30, 2026, with **CPython 3.11.8 on macOS ARM64**, using the unmodified official **Pillow 12.3.0** wheel and its bundled **libtiff 4.7.1**:
- Wheel: `pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl`
- Wheel size: `4785266` bytes
- Wheel SHA-256: `37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6`
- Pillow source revision: `bb1d8e8ab8d29048624d96e3ee53cecf7c13d13d`
- Bundled libtiff SHA-256: `e8988717b7470394a47d5ccd343130796b24ee70b6d885d3818e0e0b53238be1`
Apple's ASAN runtime was injected at process startup. This is **not an ASAN-rebuilt Pillow wheel**; allocation/memcpy interception detects the reported boundary violation, but the wheel's native instructions are not comprehensively instrumented.
Only this concrete full-package environment is established here. No affected-version range, current development-branch result, or patched release is asserted. Severity is **Unknown**; no CVSS score is proposed.
Separate earlier evidence reached the original Pillow 8.0.1 encoder constructor compiled as a component against a modern Python/libtiff environment. **That was not a complete Pillow 8.0.1 installation** and does not establish the historical package's full runtime behavior. It is not part of this reproducer or an affected-version range.
## Observed evidence and controls
The earlier integration harness performed the natural sequence `Image.open(BytesIO(input))`, `image.load()`, then `image.save(BytesIO(), format="TIFF")`, without replacing the image's metadata. Each case was recorded three times:
| Original fixture | Bytes | Recorded result |
| --- | ---: | --- |
| `colormap16` | 1685 | Loaded as `I;16`; save terminated with ASAN exit 86, READ of size 131072 crossing a 1536-byte heap region (3/3). |
| `colormap8` | 1684 | Same fixed ColorMap, but 8-bit samples; save succeeded and retained SHORT/count 768 (3/3). |
| `colormap_absent` | 137 | Same 16-bit geometry without ColorMap; save succeeded (3/3). |
Address-free summary of the recorded native diagnostic:
```text
AddressSanitizer: heap-buffer-overflow
READ of size 131072
0 bytes after 1536-byte region
Copy path: memcpy -> _TIFFmemcpy -> setByteArray -> _TIFFsetShortArrayExt
-> _TIFFVSetField -> TIFFVSetField -> ImagingLibTiffSetField
-> PyImaging_LibTiffEncoderNew
Allocation: calloc -> PyImaging_LibTiffEncoderNew
```
Input SHA-256 values (also enforced inside the script):
- `colormap16`: `5f38e0ead90b97949fb0c98c63f9c68d00ba9181db6631ecb8927376dcc75f4d`
- `colormap8`: `fdb26d9135549ed44824901985086c5ee4f06515699ca3291e498c523935405c`
- `colormap_absent`: `bd54979624f3ffb713342e0458caff9e17c477f940cda2f0960feebfabbc9556`
## Root cause and source references
- [Pillow `src/encode.c`, lines 870–899, pinned revision](https://github.com/python-pillow/Pillow/blob/bb1d8e8ab8d29048624d96e3ee53cecf7c13d13d/src/encode.c#L870): the ColorMap tuple must contain exactly 768 entries; a 1536-byte allocation is passed with a fixed stride of 256. The check does not reconcile that extent with `BitsPerSample`.
- [Pillow `src/PIL/TiffImagePlugin.py`, lines 1944–1991, pinned revision](https://github.com/python-pillow/Pillow/blob/bb1d8e8ab8d29048624d96e3ee53cecf7c13d13d/src/PIL/TiffImagePlugin.py#L1944): save carries supplied tags into the libtiff encoder and sorts the tags, so sample depth is established before ColorMap.
- [libtiff `tif_dir.c`, lines 441–448, pinned revision](https://gitlab.com/libtiff/libtiff/-/blob/5fe20d0e9aba49a6a350ed533459d1505203838f/libtiff/tif_dir.c#L441): `TIFFTAG_COLORMAP` computes the per-plane count as `1L << td->td_bitspersample` and copies that many SHORTs from each pointer.
The reported boundary is Pillow's construction of a fixed-sized array for a native API whose required size depends on metadata; this does not establish a standalone libtiff defect for a correctly sized caller buffer.
## Prior art and disclosure scope
[Pillow PR #5148](https://github.com/python-pillow/Pillow/pull/5148), merged December 28, 2020, is related TIFF metadata prior art, but its archived diff replaces a short-lived stack field-name…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/python-pillow/Pillow/security/advisories/GHSA-pchr-8j38-3x4p
- nvd.nist.gov/vuln/detail/CVE-2026-112896
- github.com/python-pillow/Pillow/commit/d03dc3c587a9b0bdc9e4304d70d59a635a87b742
- github.com/python-pillow/Pillow/releases/tag/13.0.0
- github.com/advisories/GHSA-pchr-8j38-3x4p
- github.com/python-pillow/Pillow
Merged from GitHub Advisory Database, OSV. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON