CVE-2026-112898
Pillow: Image.quantize() with empty palette leaks uninitialized stack memory
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
6.9
Pillow: Image.quantize() with empty palette leaks uninitialized stack memory
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
- Record
CVE-2026-112898- CVSS 4.0
- 6.9 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-457
- Updated
- 2026-11-02
EUVDEUVD-2026-104660
6.9
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
- Record
EUVD-2026-104660- CVSS 4.0
- 6.9 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-rfgq-q3w2-pmv2
6.9
Pillow: Image.quantize() with empty palette leaks uninitialized stack memory
============================================================================
Image.quantize() with empty palette leaks uninitialized stack memory
Location: src/libImaging/Palette.c:216 in ImagingPaletteCacheUpdate()
Commit: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
When an application lets a user supply the palette image for Pillow's Image.quantize(palette=...), the user can get back 512 bytes of the server process's stack memory in the pixels of the returned image. An 80-byte PNG with an empty palette is enough, and the leaked bytes survive saving to PNG or GIF. In the runs examined, those bytes included memory addresses inside the stack, heap, libc, libpython and Pillow's own C module, along with the stack-protector canary.
The bug is in ImagingPaletteCacheUpdate() in src/libImaging/Palette.c. It fills a local UINT8 c[512] only inside loops bounded by palette->size. When the palette has zero entries, the array is never written, but it is still copied into the colour cache. topalette() in src/libImaging/Convert.c then emits the cached values as output pixel indices. topalette() rejects a NULL palette but not one with size == 0. Three inputs produce such a zero-entry "P" image: a PNG with a zero-length PLTE chunk opened with Image.open(), a colour-mapped TGA with colour-map length 0, or a bare Image.new("P", ...). The attacker must control the palette image; the source image can be anything, and RGB sources are affected with either dither mode. There is no out-of-bounds access and no memory corruption. The consequence is a bounded stack disclosure that can reveal address layout and the canary of a long-lived worker, with no crash and no log entry. The attached reproducer quantizes a 512-colour strip against the empty-palette PNG and finds 8 of the 64 output words pointing into the process's own [stack] mapping, while a one-entry palette control yields all zeros. The behaviour was introduced in commit 54cb09d8b48c (first released in 9.1.0). The Image.new("P", ...) route exists since 10.1.0.
Details
ImagingPaletteCacheUpdate() (src/libImaging/Palette.c:154) computes, for one 8×8×8 box of the colour cache, the nearest palette index for each of the 512 slots into a local UINT8 c[512] and then copies c[] into the cache. c[] is only ever written inside for (i = 0; i < palette->size; i++). When the palette has zero entries nothing writes it, the untouched stack bytes are copied into the cache, and topalette() (src/libImaging/Convert.c:1145) emits them as the output pixels of Image.quantize(palette=...). The root cause is that topalette() only rejects a NULL palette, never one with palette->size == 0, while ImagingPaletteCacheUpdate() silently assumes size >= 1. A zero-entry "P" image is what Image.open() produces for a PNG whose PLTE chunk has length 0 (or a colour-mapped TGA with colour-map length 0); Image.new("P", size) without a subsequent putpalette() yields one as well. Both dither modes (Dither.NONE and the default Floyd-Steinberg) are affected for RGB sources (an L source takes the grayscale copy path and never consults the cache).
Showing it with the installed build, no special flags. The 80-byte PNG below is 4×4, bit depth 8, colour type 3, with a zero-length PLTE and an all-zero IDAT:
89504e470d0a1a0a 0000000d49484452 0000000400000004 0803000000 9e2f6e4c
00000000 504c5445 4ba88955
0000000b 49444154 789c6360c0040000140001 7dfe1eee
00000000 49454e44 ae426082
pal = Image.open(io.BytesIO(png)); pal.load() # pal.mode == "P", pal.im.getpalette("RGB") == b""
src = Image.new("RGB", (512, 1))
src.putdata([(r*4, g*4, b*4) for r in range(8) for g in range(8) for b in range(8)])
out = src.quantize(palette=pal, dither=Image.Dither.NONE)
out.tobytes() # 512 bytes, different in every process
The 512 source colours are…
- Record
GHSA-rfgq-q3w2-pmv2- CVSS 4.0
- 6.9 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-457
- Updated
- 2026-11-02
NVD
6.9
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
- Record
CVE-2026-112898- CVSS 4.0
- 6.9 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N - Weakness
- CWE-457
- Updated
- 2026-11-02
OSV
6.9
Pillow: Image.quantize() with empty palette leaks uninitialized stack memory
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
- Record
CVE-2026-112898- CVSS 4.0
- 6.9 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-457
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-112898
5.3
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
- Record
DEBIAN-CVE-2026-112898- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N - Updated
- 2026-11-02
Show 8 moreShow less
OSVGHSA-rfgq-q3w2-pmv2
6.9
Pillow: Image.quantize() with empty palette leaks uninitialized stack memory
============================================================================
Image.quantize() with empty palette leaks uninitialized stack memory
Location: src/libImaging/Palette.c:216 in ImagingPaletteCacheUpdate()
Commit: a707c6b21c313880d0771e54965d142dd42358b6
============================================================================
When an application lets a user supply the palette image for Pillow's Image.quantize(palette=...), the user can get back 512 bytes of the server process's stack memory in the pixels of the returned image. An 80-byte PNG with an empty palette is enough, and the leaked bytes survive saving to PNG or GIF. In the runs examined, those bytes included memory addresses inside the stack, heap, libc, libpython and Pillow's own C module, along with the stack-protector canary.
The bug is in ImagingPaletteCacheUpdate() in src/libImaging/Palette.c. It fills a local UINT8 c[512] only inside loops bounded by palette->size. When the palette has zero entries, the array is never written, but it is still copied into the colour cache. topalette() in src/libImaging/Convert.c then emits the cached values as output pixel indices. topalette() rejects a NULL palette but not one with size == 0. Three inputs produce such a zero-entry "P" image: a PNG with a zero-length PLTE chunk opened with Image.open(), a colour-mapped TGA with colour-map length 0, or a bare Image.new("P", ...). The attacker must control the palette image; the source image can be anything, and RGB sources are affected with either dither mode. There is no out-of-bounds access and no memory corruption. The consequence is a bounded stack disclosure that can reveal address layout and the canary of a long-lived worker, with no crash and no log entry. The attached reproducer quantizes a 512-colour strip against the empty-palette PNG and finds 8 of the 64 output words pointing into the process's own [stack] mapping, while a one-entry palette control yields all zeros. The behaviour was introduced in commit 54cb09d8b48c (first released in 9.1.0). The Image.new("P", ...) route exists since 10.1.0.
Details
ImagingPaletteCacheUpdate() (src/libImaging/Palette.c:154) computes, for one 8×8×8 box of the colour cache, the nearest palette index for each of the 512 slots into a local UINT8 c[512] and then copies c[] into the cache. c[] is only ever written inside for (i = 0; i < palette->size; i++). When the palette has zero entries nothing writes it, the untouched stack bytes are copied into the cache, and topalette() (src/libImaging/Convert.c:1145) emits them as the output pixels of Image.quantize(palette=...). The root cause is that topalette() only rejects a NULL palette, never one with palette->size == 0, while ImagingPaletteCacheUpdate() silently assumes size >= 1. A zero-entry "P" image is what Image.open() produces for a PNG whose PLTE chunk has length 0 (or a colour-mapped TGA with colour-map length 0); Image.new("P", size) without a subsequent putpalette() yields one as well. Both dither modes (Dither.NONE and the default Floyd-Steinberg) are affected for RGB sources (an L source takes the grayscale copy path and never consults the cache).
Showing it with the installed build, no special flags. The 80-byte PNG below is 4×4, bit depth 8, colour type 3, with a zero-length PLTE and an all-zero IDAT:
89504e470d0a1a0a 0000000d49484452 0000000400000004 0803000000 9e2f6e4c
00000000 504c5445 4ba88955
0000000b 49444154 789c6360c0040000140001 7dfe1eee
00000000 49454e44 ae426082
pal = Image.open(io.BytesIO(png)); pal.load() # pal.mode == "P", pal.im.getpalette("RGB") == b""
src = Image.new("RGB", (512, 1))
src.putdata([(r*4, g*4, b*4) for r in range(8) for g in range(8) for b in range(8)])
out = src.quantize(palette=pal, dither=Image.Dither.NONE)
out.tobytes() # 512 bytes, different in every process
The 512 source colours are…
- Record
GHSA-rfgq-q3w2-pmv2- CVSS 4.0
- 6.9 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-457
- Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-112898
6.9
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
- Record
UBUNTU-CVE-2026-112898- CVSS 4.0
- 6.9 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N - CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N - Updated
- 2026-11-04
OSVBIT-pillow-2026-112898 No score
Pillow: Image.quantize() with empty palette leaks uninitialized stack memory
Pillow is a Python imaging library. From 9.1.0 until 13.0.0, Pillow can disclose uninitialized stack memory when Image.quantize() uses an empty palette. ImagingPaletteCacheUpdate() leaves a local cache array uninitialized when the palette has zero entries, and the conversion path emits those bytes as output pixel indices. An attacker who controls the palette image can obtain stack contents from a returned PNG or GIF. This can disclose addresses and stack-canary data without an out-of-bounds access, memory corruption, or process crash. This issue is fixed in version 13.0.0.
- Record
BIT-pillow-2026-112898- Updated
- 2026-11-02
OSVCLSA-2026-1793017750 No score
TuxCare security update for pillow (9 CVEs)
TuxCare rebuilt pillow to address 9 CVEs. Fixed in 11.3.0.post11+tuxcare.
- Record
CLSA-2026-1793017750- Updated
- 2026-11-02
OSVCLSA-2026-1793085879 No score
TuxCare security update for pillow (9 CVEs)
TuxCare rebuilt pillow to address 9 CVEs. Fixed in 11.2.1.post13+tuxcare.
- Record
CLSA-2026-1793085879- Updated
- 2026-11-02
OSVCLSA-2026-1793099358 No score
TuxCare security update for pillow (8 CVEs)
TuxCare rebuilt pillow to address 8 CVEs. Fixed in 10.4.0.post15+tuxcare.
- Record
CLSA-2026-1793099358- Updated
- 2026-11-02
OSVCLSA-2026-1793346161 No score
TuxCare security update for pillow (7 CVEs)
TuxCare rebuilt pillow to address 7 CVEs. Fixed in 9.5.0.post13+tuxcare.
- Record
CLSA-2026-1793346161- Updated
- 2026-11-02
OSVopenSUSE-SU-2026:12230-1 No score
python313-Pillow-13.0.0-1.1 on GA media
These are all security issues fixed in the python313-Pillow-13.0.0-1.1 package on the GA media of openSUSE Tumbleweed.
- Record
openSUSE-SU-2026:12230-1- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
pillowPyPI |
9.1.0 up to 13.0.0 | 13.0.0 |
pillowBitnami |
9.1.0 up to 13.0.0 | 13.0.0 |
pillowTuxCare:PyPI From an advisory covering 9 CVEs |
11.3.0 up to 11.3.0.post11+tuxcare, plus listed versions 11.3.0.post1+tuxcare11.3.0.post10+tuxcare11.3.0.post2+tuxcare11.3.0.post3+tuxcare11.3.0.post4+tuxcare Show 5 moreShow less11.3.0.post5+tuxcare11.3.0.post6+tuxcare11.3.0.post7+tuxcare11.3.0.post8+tuxcare11.3.0.post9+tuxcare |
11.3.0.post11+tuxcare |
pillowTuxCare:PyPI From an advisory covering 9 CVEs |
11.2.1 up to 11.2.1.post13+tuxcare, plus listed versions 11.2.1.post1+tuxcare11.2.1.post10+tuxcare11.2.1.post11+tuxcare11.2.1.post12+tuxcare11.2.1.post2+tuxcare Show 7 moreShow less11.2.1.post3+tuxcare11.2.1.post4+tuxcare11.2.1.post5+tuxcare11.2.1.post6+tuxcare11.2.1.post7+tuxcare11.2.1.post8+tuxcare11.2.1.post9+tuxcare |
11.2.1.post13+tuxcare |
pillowTuxCare:PyPI From an advisory covering 8 CVEs |
10.4.0 up to 10.4.0.post15+tuxcare, plus listed versions 10.4.0.post1+tuxcare10.4.0.post10+tuxcare10.4.0.post11+tuxcare10.4.0.post12+tuxcare10.4.0.post13+tuxcare Show 9 moreShow less10.4.0.post14+tuxcare10.4.0.post2+tuxcare10.4.0.post3+tuxcare10.4.0.post4+tuxcare10.4.0.post5+tuxcare10.4.0.post6+tuxcare10.4.0.post7+tuxcare10.4.0.post8+tuxcare10.4.0.post9+tuxcare |
10.4.0.post15+tuxcare |
pillowTuxCare:PyPI From an advisory covering 7 CVEs |
9.5.0 up to 9.5.0.post13+tuxcare, plus listed versions 9.5.0.post1+tuxcare9.5.0.post10+tuxcare9.5.0.post11+tuxcare9.5.0.post12+tuxcare9.5.0.post2+tuxcare Show 7 moreShow less9.5.0.post3+tuxcare9.5.0.post4+tuxcare9.5.0.post5+tuxcare9.5.0.post6+tuxcare9.5.0.post7+tuxcare9.5.0.post8+tuxcare9.5.0.post9+tuxcare |
9.5.0.post13+tuxcare |
pillowDebian:12 |
All versions, plus listed versions 10.0.0-110.1.0-110.2.0-110.3.0-110.3.0-2 Show 19 moreShow less10.4.0-110.4.0-1.111.1.0-111.1.0-211.1.0-311.1.0-411.1.0-511.2.1-111.3.0-112.0.0-112.1.0-112.1.1-112.1.1-212.2.0-112.3.0-113.0.0-19.4.0-1.19.4.0-1.1+deb12u19.5.0-1 |
None known |
pillowDebian:13 |
All versions, plus listed versions 11.1.0-511.1.0-5+deb13u111.1.0-5+deb13u211.1.0-5+deb13u311.1.0-5+deb13u4 Show 9 moreShow less11.2.1-111.3.0-112.0.0-112.1.0-112.1.1-112.1.1-212.2.0-112.3.0-113.0.0-1 |
None known |
pillowDebian:14 |
All versions, plus listed versions 11.1.0-511.2.1-111.3.0-112.0.0-112.1.0-1 Show 5 moreShow less12.1.1-112.1.1-212.2.0-112.3.0-113.0.0-1 |
None known |
pillowPyPI |
9.1.0 up to 13.0.0, plus listed versions 10.0.010.0.110.1.010.2.010.3.0 Show 15 moreShow less10.4.011.0.011.1.011.2.111.3.012.0.012.1.012.1.112.2.012.3.09.1.19.2.09.3.09.4.09.5.0 |
13.0.0 |
Show 5 moreShow less
| Package | Affected | Fixed |
|---|---|---|
pillow-python2Ubuntu:Pro:20.04:LTS |
All versions, plus listed versions 6.2.1-3~build16.2.1-36.2.1-3ubuntu0.1~esm16.2.1-3ubuntu0.1~esm2 |
None known |
pillowUbuntu:24.04:LTS |
All versions, plus listed versions 10.0.0-110.1.0-110.2.0-110.2.0-1build110.2.0-1ubuntu110.2.0-1ubuntu1.210.2.0-1ubuntu1.310.2.0-1ubuntu1.4 |
None known |
pillowUbuntu:26.10 |
All versions, plus listed versions 12.1.1-2ubuntu112.3.0-1ubuntu1 |
None known |
pillowUbuntu:26.04:LTS |
All versions, plus listed versions 11.3.0-1ubuntu111.3.0-1ubuntu212.0.0-1ubuntu112.1.1-1ubuntu112.1.1-2ubuntu1 Show 4 moreShow less12.1.1-2ubuntu1.112.1.1-2ubuntu1.212.1.1-2ubuntu1.312.1.1-2ubuntu1.4 |
None known |
python-PillowopenSUSE:Tumbleweed From an advisory covering 14 CVEs |
before 13.0.0-1.1 | 13.0.0-1.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected | ||
| affected | OSV | |
| affected, facts | OSV | |
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-112898 is a medium vulnerability (CVSS 6.9) in Python Pillow, published October 19, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.34%, the 27th percentile. Fixed in pillow 13.0.0.
References
github.com/python-pillow/Pillow/security/advisories/GHSA-rfgq-q3w2-pmv2
github.com/python-pillow/Pillow/commit/13943c8f857b22f073d30f0ef9b725a0d255f1a4
github.com/python-pillow/Pillow/releases/tag/13.0.0
nvd.nist.gov/vuln/detail/CVE-2026-112898
github.com/advisories/GHSA-rfgq-q3w2-pmv2
github.com/CVEProject/cvelistV5/tree/main/cves/2026/112xxx/CVE-2026-112898.json
Show 18 moreShow less
security-tracker.debian.org/tracker/CVE-2026-112898
github.com/python-pillow/Pillow
ubuntu.com/security/CVE-2026-112898
cve.org/CVERecord?id=CVE-2026-112898
suse.com/security/cve/CVE-2026-112390
suse.com/security/cve/CVE-2026-112392
suse.com/security/cve/CVE-2026-112394
suse.com/security/cve/CVE-2026-112396
suse.com/security/cve/CVE-2026-112398
suse.com/security/cve/CVE-2026-112400
suse.com/security/cve/CVE-2026-112402
suse.com/security/cve/CVE-2026-112404
suse.com/security/cve/CVE-2026-112406
suse.com/security/cve/CVE-2026-112892
suse.com/security/cve/CVE-2026-112894
suse.com/security/cve/CVE-2026-112896
suse.com/security/cve/CVE-2026-112898
suse.com/security/cve/CVE-2026-112900
Left out here: the rest of 2 source descriptions. The JSON record has them.