Neutron, nuestro motor de IA, obtuvo un 97.1% en el benchmark CyberGym de UC Berkeley. Más información

Análisis a fondo

Análisis a fondo

RCE por desbordamiento del heap de FlashPix en Pillow (CVE-2026-112398)

Decodificar una imagen FlashPix manipulada en Pillow 12.3.0 o anterior con olefile instalado permite al atacante ejecutar comandos; Pillow 13.0.0 corrige CVE-2026-112398.

Resumen

CVE-2026-112398 es un desbordamiento de búfer en el heap de severidad crítica (CVSS v4.0 9.2) en el decodificador JPEG de FlashPix de Pillow. Puede ejecutar comandos elegidos por el atacante cuando una aplicación decodifica una imagen no confiable, en todas las versiones hasta 12.3.0 con olefile instalado.

  • El plugin FlashPix de Pillow decodifica mosaicos JPEG dentro de un contenedor OLE2, el formato Compound File Binary Format de Microsoft. Dimensiona el búfer de decodificación según el modo de color declarado en los metadatos FlashPix y luego permite que libjpeg emita filas según el número de componentes del JPEG incrustado. Un archivo que declara 2 bytes por píxel mientras su JPEG emite 4 bytes por píxel desborda el búfer de decodificación en cada fila.
  • Cualquier servicio que abra y decodifique cargas no confiables está expuesto si utiliza Pillow hasta 12.3.0 con el paquete opcional olefile instalado: el archivo se detecta por su firma de contenido, no por su extensión, y el desbordamiento se activa con una simple llamada Image.open().load() sin interacción del usuario.
  • El proveedor asigna CWE-122 (desbordamiento de búfer en el heap) y CWE-787 (escritura fuera de límites), y su aviso incluye una prueba de concepto que ejecuta un comando en Pillow 12.2.0 estándar. El enriquecimiento de Authorized Data Publisher (ADP) de CISA marca la evidencia de explotación como «poc». No figura en el catálogo KEV y no se conocen ataques reales a fecha del 24 de octubre de 2026.
  • Actualice a Pillow 13.0.0, publicado el 16 de octubre de 2026. Si no puede, desinstale olefile, lo que desactiva la compatibilidad con FlashPix y Microsoft Image Composer, o rechace archivos OLE2 antes de que lleguen a Pillow.

¿Estoy afectado?

Está afectado si se cumplen todas estas condiciones:

  • Su aplicación, o algo que importa, abre archivos no confiables con Image.open() y luego fuerza la decodificación mediante .load(), .thumbnail(), .tobytes() o cualquier otra llamada que lea datos de píxeles.
  • La versión instalada de Pillow es 12.3.0 o anterior. Todas las versiones anteriores están afectadas; el registro CVE enumera el intervalo anterior a 13.0.0.
  • El paquete opcional olefile está instalado en ese entorno. La detección de FlashPix lo importa, y el empaquetado actual de Pillow lo declara como extra opcional para las funcionalidades fpx y mic.

No está afectado si el procesamiento de imágenes lo realiza Pillow 13.0.0 o posterior, o si olefile está ausente, en cuyo caso el plugin FlashPix nunca se carga.

PyPI registró 141.847.434 descargas de pillow en la última semana (consultado el 24 de octubre de 2026). Pillow suele llegar como dependencia transitiva, por lo que debe auditar todos los entornos, no solo los proyectos que lo declaran directamente.

Rama Versiones afectadas Versión corregida
13.x ninguna 13.0.0 y posteriores
12.x y todas las anteriores <= 12.3.0 13.0.0

Compruebe qué ejecutan sus entornos, incluidas las imágenes de contenedor y los entornos de compilación:

pip show pillow | grep -E "^(Name|Version)"
python -c "import PIL; print(PIL.__version__)"
python -c "import olefile; print('olefile', olefile.__version__)" 2>/dev/null || echo "olefile absent"
# pip requirements files
grep -i "^pillow" requirements*.txt

# Pipenv and pyproject.toml
grep -i "pillow" Pipfile.lock pyproject.toml 2>/dev/null

# Poetry
grep -A2 "name = \"pillow\"" poetry.lock

# Container images
docker run --rm <image> python -c "import PIL; print(PIL.__version__)"

¿Cómo lo corrijo?

Actualice a la versión corregida:

pip install --upgrade "pillow>=13.0.0"

Pillow 13.0.0 dejó de admitir Python 3.10, por lo que los entornos fijados en 3.10 necesitan actualizar Python o aplicar la medida provisional siguiente.

Si aún no puede actualizar, elimine la dependencia que lo habilita. La compatibilidad con FlashPix necesita olefile, por lo que desinstalarlo desactiva la ruta vulnerable:

pip uninstall -y olefile

Esto elimina la compatibilidad con FlashPix y Microsoft Image Composer (MIC), los formatos que el aviso identifica como dependientes de olefile. El límite es claro: un servicio que procese legítimamente archivos FlashPix o MIC pierde esa capacidad, y también deja de funcionar cualquier otro código del entorno que importe olefile.

Como alternativa, una puerta de enlace puede rechazar cargas que comiencen con la firma OLE2 D0CF11E0A1B11AE1, la misma que identifica el plugin, lo que detiene FlashPix en la entrada con independencia de la extensión del archivo.

¿Cuál es la causa raíz?

El plugin FlashPix dimensiona su búfer de decodificación JPEG a partir de los metadatos FlashPix, mientras libjpeg escribe filas cuyo tamaño procede del encabezado JPEG incrustado, y nada comparaba ambas medidas.

El plugin FlashPix asigna identificadores de color del contenedor a modos de Pillow, en src/PIL/FpxImagePlugin.py de 12.2.0, líneas 24 a 33:

MODES = {
    # opacity
    (0x00007FFE,): ("A", "L"),
    # monochrome
    (0x00010000,): ("L", "L"),
    (0x00018000, 0x00017FFE): ("RGBA", "LA"),
    ...
    (0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
}

El atacante elige qué fila de la tabla se aplica escribiendo esos identificadores de color en el archivo. La fila ("RGBA", "LA") selecciona el rawmode LA, de 2 bytes por píxel. El decodificador reserva el búfer de línea de exploración a partir de ese rawmode, en src/decode.c de 12.2.0, líneas 223 a 226:

state->bytes = (state->bits * state->xsize + 7) / 8;   // "LA" gives 2 * xsize bytes per row
...
state->buffer = (UINT8 *)calloc(1, state->bytes);

Después, el decodificador JPEG asigna la cadena rawmode a un espacio de color de salida de libjpeg, en src/libImaging/JpegDecode.c de 12.2.0. Los rawmode reconocidos, como CMYK, YCbCr y YCbCrK, reciben un espacio de color concreto. LA y YCC;P no se reconocen, por lo que el control llega al caso alternativo de las líneas 219 a 221:

/* Disable decoder conversions. */
context->cinfo.jpeg_color_space = JCS_UNKNOWN;
context->cinfo.out_color_space  = JCS_UNKNOWN;

Con JCS_UNKNOWN, libjpeg no convierte el color y emite num_components bytes por píxel, un valor que el atacante escribe en el marcador SOF del JPEG incrustado. Si se establece en 4 con un búfer dimensionado para 2, cada llamada a jpeg_read_scanlines en la línea 252 escribe una fila de 4 componentes en un búfer de 2 bytes por píxel:

ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1);   // writes 4 * width into a 2 * width buffer

El atacante controla la longitud del desbordamiento mediante el ancho de la imagen. Los bytes que desbordan son los píxeles decodificados, que el atacante moldea mediante el flujo JPEG en lugar de escribir directamente. La comprobación de firma que hace accesible esta ruta, al principio del mismo archivo del plugin:

def _accept(prefix: bytes) -> bool:
    return prefix.startswith(olefile.MAGIC)

Es la firma OLE2, que se compara con el contenido de cada archivo que se pide abrir a Pillow.

¿Cómo se explota?

El atacante crea un archivo FlashPix cuyos metadatos declaran 2 bytes por píxel, pero cuyo JPEG incrustado declara 4 componentes. Decodificarlo desborda el búfer del heap, y la PoC del aviso lo convierte en un secuestro del puntero de tipo que llama a system().

La ruta de ataque, tal como demuestra la prueba de concepto del aviso:

  1. El atacante construye un archivo FlashPix independiente en su propio equipo utilizando el generador de payloads del aviso. Construirlo no requiere acceso al objetivo.
  2. El archivo declara el modo ("RGBA", "LA"), por lo que Pillow dimensiona el búfer del mosaico a 2 bytes por píxel, e incrusta un JPEG cuyo SOF declara 4 componentes, por lo que libjpeg emite 4 bytes por píxel.
  3. El servicio víctima recibe el archivo mediante cualquier ruta de carga o descarga y fuerza la decodificación vulnerable. Toda la operación de la PoC es Image.open(path).load():
#!/usr/bin/env python3
# The entire vulnerable operation an application performs on an untrusted file.
import sys
from PIL import Image

Image.open(sys.argv[1]).load()
  1. La firma OLE2 del encabezado selecciona el plugin FlashPix, independientemente del nombre del archivo. El desbordamiento ocurre en el primer load().
  2. El desbordamiento sobrescribe un objeto bytes adyacente al búfer de decodificación e introduce un objeto de tipo falso y una vtable en la misma memoria controlada del heap. Cuando Pillow libera el búfer de entrada, PyBuffer_Release sigue el puntero de tipo secuestrado y realiza una llamada a través de él. La PoC dirige esa llamada a system@plt con la cadena del comando en el registro del primer argumento, y system("id") se ejecuta como el usuario que procesa las imágenes.

La PoC publicada se ejecuta contra una imagen Docker fijada con ASLR desactivado para que funcionen sus direcciones incrustadas; id se ejecuta y después el intérprete aborta por la corrupción del heap con corrupted size vs. prev_size, una vez ejecutado el comando. Contra un servidor normal, el mismo desbordamiento debe gestionar la aleatorización de direcciones, por lo que la prueba de concepto demuestra el error y la ruta para convertirlo en un exploit, no un exploit universal de un solo intento.

¿Cómo se activa el desbordamiento de FlashPix?

En una pasada, la firma OLE2 selecciona el plugin FlashPix, el rawmode LA dimensiona el búfer de línea a 2 bytes por píxel, JCS_UNKNOWN hace que libjpeg emita los 4 componentes JPEG y la fila sobredimensionada alcanza el siguiente objeto del heap.

flowchart LR A["Archivo FPX manipulado
cargado en el servicio"] --> B["Image.open() reconoce
la firma OLE2 D0CF11E0"] B --> C["Los ID de color FlashPix seleccionan
rawmode LA, 2 bytes por píxel"] C --> D["Búfer de decodificación reservado
con 2 * width bytes"] D --> E["rawmode LA no reconocido,
libjpeg usa JCS_UNKNOWN"] E --> F["El JPEG incrustado declara 4
componentes, filas escritas
a 4 bytes por píxel"] F --> G["Escritura fuera de límites del heap
con contenido del atacante"] G --> H["Puntero de tipo del objeto
adyacente sobrescrito"] H --> I["El hook de liberación llama al
destino atacante y ejecuta código"]

¿Qué puede hacer un atacante?

Decodificar el archivo manipulado ejecuta comandos elegidos por el atacante como el usuario que procesa las imágenes, lo que supone el compromiso completo de ese proceso y de sus datos y privilegios. El aviso define el impacto como ejecución arbitraria de comandos en el contexto de ese proceso. Las condiciones:

  • Acceso de red a un endpoint que abra archivos en los que pueda influir el atacante y fuerce su decodificación (.load(), .thumbnail(), .tobytes()), con la dependencia opcional olefile instalada. El aviso da como ejemplos servicios de carga y miniaturas, procesamiento de avatares y medios, y pipelines de conversión de documentos e imágenes.
  • Sin interacción del usuario ni trucos con extensiones: la detección se basa en el contenido, por lo que un archivo llamado avatar.png con bytes OLE2 sigue la ruta FlashPix.
  • La ejecución de un solo intento de la PoC se demuestra en una imagen fijada con ASLR desactivado; por tanto, un objetivo real con aleatorización de direcciones exige trabajo adicional de explotación, mientras que la escritura fuera de límites subyacente ocurre en todas las versiones afectadas.

¿Por qué funciona la corrección?

La corrección añade una comprobación de límites en ImagingJpegDecode antes de leer cualquier línea de exploración, rechazando flujos cuyo ancho de salida o tamaño de fila supere el búfer de decodificación. El commit del parche, integrado como #10131, añade esta comprobación:

+            /* The scanline buffer was sized from the tile extents and the
+               unpacker, not from the JPEG header, so make sure the lines
+               libjpeg is about to produce actually fit in it. */
+            if (context->cinfo.output_width > (JDIMENSION)state->xsize ||
+                (size_t)context->cinfo.output_components * context->cinfo.output_width >
+                    (size_t)state->bytes) {
+                jpeg_destroy_decompress(&context->cinfo);
+                state->errcode = IMAGING_CODEC_BROKEN;
+                return -1;
+            }

La comprobación rechaza un JPEG cuyo ancho supere el mosaico o cuyos componentes superen el tamaño por píxel del búfer, destruye el descompresor e informa de «flujo de datos roto» en lugar de decodificar. Esto cierra la clase de discrepancia allí donde finalmente coinciden el tamaño del búfer y el encabezado JPEG: no en la tabla FlashPix, que sigue asignando modos LA, sino en el decodificador, que ahora se niega a escribir filas que no caben.

Es una corrección completa para entradas JPEG con líneas de exploración sobredimensionadas; no elimina la ruta JCS_UNKNOWN en sí, que sigue siendo correcta mientras las filas quepan en el búfer.

¿Cómo compruebo si he sufrido una intrusión?

No se han publicado indicadores de compromiso, dominios ni infraestructura de atacantes para este problema. El desbordamiento deja tres tipos de evidencias que buscar en su entorno:

  • Registros de caída de procesos de procesamiento de imágenes con mensajes de corrupción del heap, como el aborto corrupted size vs. prev_size que deja la PoC, o terminaciones SIGSEGV de procesos que abren imágenes cargadas.
  • Archivos cargados cuyos primeros 8 bytes sean la firma OLE2 D0 CF 11 E0 A1 B1 1A E1, pero cuyo nombre o tipo declarado no sea FlashPix: es la firma de entrega que describe la comprobación del propio plugin.
  • Tareas de procesamiento que abrieron una imagen y luego ejecutaron comandos o iniciaron procesos inesperados, ya que la ejecución ocurre en el contexto del propio proceso de trabajo.

Cronología

  • 2026-10-06: Andrew Murray (radarhere) incorpora la comprobación de límites de líneas de exploración, integrada como #10131.
  • 2026-10-16: Pillow publica 13.0.0 con la corrección.
  • 2026-10-17: Pillow publica el aviso GHSA-5x8w-8jj5-q52h, calificado como crítico, CVSS v4.0 9.2, con una prueba de concepto completa y reconocimiento a Alex Bouffard (pebwalker).
  • 2026-10-19: La CNA de GitHub publica el registro de CVE-2026-112398.
  • 2026-10-22: El bloque de enriquecimiento Authorized Data Publisher (ADP) de CISA registra la evidencia de explotación como «poc».
  • 2026-10-23: GitHub Advisory Database publica y revisa el aviso.
  • 2026-10-24: Este artículo.

Preguntas frecuentes

¿Debe mi servicio tener olefile instalado para estar afectado?

Sí. El plugin FlashPix importa olefile y el empaquetado de Pillow lo declara como dependencia opcional para los extras fpx y mic. Sin él, el plugin nunca se carga y el archivo OLE2 se rechaza antes de decodificarse. Los entornos que nunca lo instalaron no siguen la ruta vulnerable.

¿Lo detiene renombrar o bloquear cargas .fpx?

No. Image.open() selecciona el plugin FlashPix por los bytes de firma OLE2 al principio del archivo, de modo que la extensión no importa ni para el ataque ni para la defensa. Rechazar archivos por el prefijo de firma D0CF11E0A1B11AE1 en el límite de carga sí lo detiene, porque esa es la misma firma que exige el plugin.

¿La PoC publicada es un exploit remoto completo?

Es una demostración funcional de ejecución de comandos, pero apunta a una imagen Docker fijada con ASLR desactivado para que las direcciones incrustadas del payload sean constantes. En un servidor con aleatorización de direcciones, el mismo desbordamiento requiere trabajo adicional para colocar o filtrar direcciones. El desbordamiento en sí y el control del heap que concede al atacante se comportan igual en cualquier versión afectada.

¿También son peligrosos los archivos JPEG normales?

Un JPEG normal no sigue esta ruta. El error necesita el contenedor FlashPix para seleccionar el rawmode LA y el caso alternativo JCS_UNKNOWN, algo que nunca ocurre al abrir JPEG ordinarios. El parche también rechaza ahora cualquier JPEG cuyas filas superen el búfer, por lo que la clase general queda cerrada a nivel del decodificador.

¿Qué más corrigió Pillow 13.0.0?

La misma versión también corrigió CVE-2026-112900, una inyección de argumentos en la llamada a Ghostscript para entradas EPS que afecta a todos los Pillow desde 2.6.0 y que el proyecto calificó como crítica, CVSS v4.0 9.2. Actualizar a 13.0.0 cubre ambos problemas a la vez.

Revise sus servicios de procesamiento de imágenes

Inicie una evaluación de seguridad con Ostorlab y utilice las comprobaciones de dependencias anteriores para verificar la versión de Pillow y la presencia de olefile en cada entorno de procesamiento de imágenes. Actualice los despliegues afectados a Pillow 13.0.0 o posterior.

Referencias

  • Aviso de Pillow GHSA-5x8w-8jj5-q52h con la prueba de concepto y el laboratorio Docker, publicado el 17 de octubre de 2026: https://github.com/python-pillow/Pillow/security/advisories/GHSA-5x8w-8jj5-q52h (consultado el 24 de octubre de 2026)
  • Registro CVE-2026-112398 con el vector CVSS v4.0 9.2 y el enriquecimiento «poc» de CISA ADP: https://cveawg.mitre.org/api/cve/CVE-2026-112398 (consultado el 24 de octubre de 2026)
  • Entrada de GitHub Advisory Database: https://github.com/advisories/GHSA-5x8w-8jj5-q52h (consultado el 24 de octubre de 2026)
  • Commit de corrección 242a059a, «Rechazar flujos JPEG cuyas líneas de exploración sean más anchas o tengan más componentes», fechado el 6 de octubre de 2026: https://github.com/python-pillow/Pillow/commit/242a059aedc1935097d9aa1dc3f099750467ba75 (consultado el 24 de octubre de 2026)
  • Versión Pillow 13.0.0, publicada el 16 de octubre de 2026: https://github.com/python-pillow/Pillow/releases/tag/13.0.0 (consultado el 24 de octubre de 2026)
  • Código vulnerable de FpxImagePlugin en 12.2.0: https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/src/PIL/FpxImagePlugin.py (consultado el 24 de octubre de 2026)
  • Código vulnerable de JpegDecode.c en 12.2.0: https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/src/libImaging/JpegDecode.c (consultado el 24 de octubre de 2026)
  • pyproject.toml de Pillow que declara olefile como extra opcional de fpx y mic: https://raw.githubusercontent.com/python-pillow/Pillow/12.2.0/pyproject.toml (consultado el 24 de octubre de 2026)
  • Estadísticas de descargas de pillow en PyPI, 141.847.434 descargas en la última semana: https://pypistats.org/packages/pillow (consultado el 24 de octubre de 2026)
  • Aviso complementario de la inyección de argumentos Ghostscript corregida en la misma versión: https://github.com/python-pillow/Pillow/security/advisories/GHSA-w889-685w-c8fh (consultado el 24 de octubre de 2026)